diff options
Diffstat (limited to 'src/widget')
| -rw-r--r-- | src/widget/form.js | 51 | ||||
| -rw-r--r-- | src/widget/input.js | 89 | ||||
| -rw-r--r-- | src/widget/select.js | 554 | 
3 files changed, 342 insertions, 352 deletions
| diff --git a/src/widget/form.js b/src/widget/form.js index 962cb6b8..405aae74 100644 --- a/src/widget/form.js +++ b/src/widget/form.js @@ -82,28 +82,31 @@        </doc:scenario>      </doc:example>   */ -angularWidget('form', function(form){ -  this.descend(true); -  this.directives(true); -  return ['$formFactory', '$element', function($formFactory, formElement) { -    var name = formElement.attr('name'), -        parentForm = $formFactory.forElement(formElement), -        form = $formFactory(parentForm); -    formElement.data('$form', form); -    formElement.bind('submit', function(event) { -      if (!formElement.attr('action')) event.preventDefault(); -    }); -    if (name) { -      this[name] = form; +var ngFormDirective = ['$formFactory', function($formFactory) { +  return { +    restrict: 'E', +    compile: function() { +      return { +        pre: function(scope, formElement, attr) { +          var name = attr.name, +            parentForm = $formFactory.forElement(formElement), +            form = $formFactory(parentForm); +          formElement.data('$form', form); +          formElement.bind('submit', function(event){ +            if (!attr.action) event.preventDefault(); +          }); +          if (name) { +            scope[name] = form; +          } +          watch('valid'); +          watch('invalid'); +          function watch(name) { +            form.$watch('$' + name, function(value) { +              formElement[value ? 'addClass' : 'removeClass']('ng-' + name); +            }); +          } +        } +      };      } -    watch('valid'); -    watch('invalid'); -    function watch(name) { -      form.$watch('$' + name, function(value) { -        formElement[value ? 'addClass' : 'removeClass']('ng-' + name); -      }); -    } -  }]; -}); - -angularWidget('ng:form', angularWidget('form')); +  }; +}]; diff --git a/src/widget/input.js b/src/widget/input.js index e666a0c1..9f9d9852 100644 --- a/src/widget/input.js +++ b/src/widget/input.js @@ -542,23 +542,23 @@ angularInputType('checkbox', function(inputElement, widget) {        </doc:scenario>      </doc:example>   */ -angularInputType('radio', function(inputElement, widget) { +angularInputType('radio', function(inputElement, widget, attr) {    //correct the name -  inputElement.attr('name', widget.$id + '@' + inputElement.attr('name')); +  attr.$set('name', widget.$id + '@' + attr.name);    inputElement.bind('click', function() {      widget.$apply(function() {        if (inputElement[0].checked) { -        widget.$emit('$viewChange', widget.$value); +        widget.$emit('$viewChange', attr.value);        }      });    });    widget.$render = function() { -    inputElement[0].checked = isDefined(widget.$value) && (widget.$value == widget.$viewValue); +    inputElement[0].checked = isDefined(attr.value) && (attr.value == widget.$viewValue);    };    if (inputElement[0].checked) { -    widget.$viewValue = widget.$value; +    widget.$viewValue = attr.value;    }  }); @@ -664,28 +664,28 @@ var HTML5_INPUTS_TYPES =  makeMap(        </doc:source>        <doc:scenario>          it('should initialize to model', function() { -          expect(binding('user')).toEqual('{\n  \"last\":\"visitor",\n  \"name\":\"guest\"}'); +          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('{\n  \"last\":\"visitor",\n  \"name\":\"\"}'); +          expect(binding('user')).toEqual('{"last":"visitor","name":""}');            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('{\n  \"last\":\"",\n  \"name\":\"guest\"}'); +          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('{\n  \"last\":\"xx",\n  \"name\":\"guest\"}'); +          expect(binding('user')).toEqual('{"last":"xx","name":"guest"}');            expect(binding('myForm.lastName.$valid')).toEqual('false');            expect(binding('myForm.lastName.$error')).toMatch(/MINLENGTH/);            expect(binding('myForm.$valid')).toEqual('false'); @@ -694,7 +694,7 @@ var HTML5_INPUTS_TYPES =  makeMap(          it('should be valid if longer than max length', function() {            input('user.last').enter('some ridiculously long name');            expect(binding('user')) -            .toEqual('{\n  \"last\":\"some ridiculously long name",\n  \"name\":\"guest\"}'); +            .toEqual('{"last":"some ridiculously long name","name":"guest"}');            expect(binding('myForm.lastName.$valid')).toEqual('false');            expect(binding('myForm.lastName.$error')).toMatch(/MAXLENGTH/);            expect(binding('myForm.$valid')).toEqual('false'); @@ -702,26 +702,24 @@ var HTML5_INPUTS_TYPES =  makeMap(        </doc:scenario>      </doc:example>   */ -angularWidget('input', function(inputElement){ -  this.directives(true); -  this.descend(true); -  var modelExp = inputElement.attr('ng:model'); -  return modelExp && -    ['$defer', '$formFactory', '$element', -        function($defer, $formFactory, inputElement) { +var inputDirective = ['$defer', '$formFactory', function($defer, $formFactory) { +  return { +    restrict: 'E', +    link: function(modelScope, inputElement, attr) { +      if (!attr.ngModel) return; +        var form = $formFactory.forElement(inputElement),            // We have to use .getAttribute, since jQuery tries to be smart and use the            // type property. Trouble is some browser change unknown to text. -          type = inputElement[0].getAttribute('type') || 'text', +          type = attr.type || 'text',            TypeController, -          modelScope = this,            patternMatch, widget, -          pattern = trim(inputElement.attr('ng:pattern')), -          minlength = parseInt(inputElement.attr('ng:minlength'), 10), -          maxlength = parseInt(inputElement.attr('ng:maxlength'), 10), +          pattern = attr.ngPattern, +          modelExp = attr.ngModel, +          minlength = parseInt(attr.ngMinlength, 10), +          maxlength = parseInt(attr.ngMaxlength, 10),            loadFromScope = type.match(/^\s*\@\s*(.*)/); -         if (!pattern) {           patternMatch = valueFn(true);         } else { @@ -743,7 +741,7 @@ angularWidget('input', function(inputElement){        type = lowercase(type);        TypeController = (loadFromScope -              ? (assertArgFn(this.$eval(loadFromScope[1]), loadFromScope[1])).$unboundFn +              ? (assertArgFn(modelScope.$eval(loadFromScope[1]), loadFromScope[1])).$unboundFn                : angularInputType(type)) || noop;        if (!HTML5_INPUTS_TYPES[type]) { @@ -757,26 +755,21 @@ angularWidget('input', function(inputElement){        }        //TODO(misko): setting $inject is a hack -      !TypeController.$inject && (TypeController.$inject = ['$element', '$scope']); +      !TypeController.$inject && (TypeController.$inject = ['$element', '$scope', '$attr']);        widget = form.$createWidget({            scope: modelScope,            model: modelExp, -          onChange: inputElement.attr('ng:change'), -          alias: inputElement.attr('name'), +          onChange: attr.ngChange, +          alias: attr.name,            controller: TypeController, -          controllerArgs: {$element: inputElement} +          controllerArgs: {$element: inputElement, $attr: attr}        }); -      watchElementProperty(this, widget, 'value', inputElement); -      watchElementProperty(this, widget, 'required', inputElement); -      watchElementProperty(this, widget, 'readonly', inputElement); -      watchElementProperty(this, widget, 'disabled', inputElement); -        widget.$pristine = !(widget.$dirty = false);        widget.$on('$validate', function() {          var $viewValue = trim(widget.$viewValue), -            inValid = widget.$required && !$viewValue, +            inValid = attr.required && !$viewValue,              tooLong = maxlength && $viewValue && $viewValue.length > maxlength,              tooShort = minlength && $viewValue && $viewValue.length < minlength,              missMatch = $viewValue && !patternMatch($viewValue); @@ -812,7 +805,7 @@ angularWidget('input', function(inputElement){            inputElement.val(widget.$viewValue || '');          }; -        inputElement.bind('keydown change input', function(event) { +        inputElement.bind('keydown change input', function(event){            var key = event.keyCode;            if (/*command*/   key != 91 &&                /*modifiers*/ !(15 < key && key < 19) && @@ -827,8 +820,9 @@ angularWidget('input', function(inputElement){            }          });        } -    }]; -}); +    } +  }; +}];  /** @@ -856,24 +850,3 @@ angularWidget('input', function(inputElement){   * @param {string=} ng:change Angular expression to be executed when input changes due to user   *    interaction with the input element.   */ -angularWidget('textarea', angularWidget('input')); - - -function watchElementProperty(modelScope, widget, name, element) { -  var bindAttr = fromJson(element.attr('ng:bind-attr') || '{}'), -      match = /\s*\{\{(.*)\}\}\s*/.exec(bindAttr[name]), -      isBoolean = BOOLEAN_ATTR[name]; -  widget['$' + name] = isBoolean -    ? ( // some browsers return true some '' when required is set without value. -        isString(element.prop(name)) || !!element.prop(name) || -        // this is needed for ie9, since it will treat boolean attributes as false -        !!element[0].attributes[name]) -    : element.attr(name); -  if (bindAttr[name] && match) { -    modelScope.$watch(match[1], function(value) { -      widget['$' + name] = isBoolean ? !!value : value; -      widget.$emit('$validate'); -      widget.$render && widget.$render(); -    }); -  } -} diff --git a/src/widget/select.js b/src/widget/select.js index a3633ddd..ae9e1b7c 100644 --- a/src/widget/select.js +++ b/src/widget/select.js @@ -112,321 +112,335 @@        </doc:source>        <doc:scenario>           it('should check ng:options', function() { -           expect(binding('color')).toMatch('red'); +           expect(binding('{selected_color:color}')).toMatch('red');             select('color').option('0'); -           expect(binding('color')).toMatch('black'); +           expect(binding('{selected_color:color}')).toMatch('black');             using('.nullable').select('color').option(''); -           expect(binding('color')).toMatch('null'); +           expect(binding('{selected_color:color}')).toMatch('null');           });        </doc:scenario>      </doc:example>   */ - -                       //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+(.*)$/; - - -angularWidget('select', function(element){ -  this.directives(true); -  this.descend(true); -  return element.attr('ng:model') && -               ['$formFactory', '$compile', '$parse', '$element', -        function($formFactory,   $compile,   $parse,   selectElement){ -    var modelScope = this, -        match, -        form = $formFactory.forElement(selectElement), -        multiple = selectElement.attr('multiple'), -        optionsExp = selectElement.attr('ng:options'), -        modelExp = selectElement.attr('ng:model'), -        widget = form.$createWidget({ -          scope: modelScope, -          model: modelExp, -          onChange: selectElement.attr('ng:change'), -          alias: selectElement.attr('name'), -          controller: ['$scope', optionsExp ? Options : (multiple ? Multiple : Single)]}); - -    selectElement.bind('$destroy', function() { widget.$destroy(); }); - -    widget.$pristine = !(widget.$dirty = false); - -    watchElementProperty(modelScope, widget, 'required', selectElement); -    watchElementProperty(modelScope, widget, 'readonly', selectElement); -    watchElementProperty(modelScope, widget, 'disabled', selectElement); - -    widget.$on('$validate', function() { -      var valid = !widget.$required || !!widget.$modelValue; -      if (valid && multiple && widget.$required) valid = !!widget.$modelValue.length; -      if (valid !== !widget.$error.REQUIRED) { -        widget.$emit(valid ? '$valid' : '$invalid', 'REQUIRED'); -      } -    }); - -    widget.$on('$viewChange', function() { -      widget.$pristine = !(widget.$dirty = true); -    }); - -    forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) { -      widget.$watch('$' + name, function(value) { -        selectElement[value ? 'addClass' : 'removeClass']('ng-' + name); +var ngOptionsDirective = valueFn({ terminal: true }); +var selectDirective = ['$formFactory', '$compile', '$parse', +               function($formFactory,   $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', +    link: function(modelScope, selectElement, attr) { +      if (!attr.ngModel) return; +      var form = $formFactory.forElement(selectElement), +          multiple = attr.multiple, +          optionsExp = attr.ngOptions, +          modelExp = attr.ngModel, +          widget = form.$createWidget({ +            scope: modelScope, +            model: modelExp, +            onChange: attr.ngChange, +            alias: attr.name, +            controller: ['$scope', optionsExp ? Options : (multiple ? Multiple : Single)]}); + +      selectElement.bind('$destroy', function() { widget.$destroy(); }); + +      widget.$pristine = !(widget.$dirty = false); + +      widget.$on('$validate', function() { +        var valid = !attr.required || !!widget.$modelValue; +        if (valid && multiple && attr.required) valid = !!widget.$modelValue.length; +        if (valid !== !widget.$error.REQUIRED) { +          widget.$emit(valid ? '$valid' : '$invalid', 'REQUIRED'); +        }        }); -    }); -    //////////////////////////// +      widget.$on('$viewChange', function() { +        widget.$pristine = !(widget.$dirty = true); +      }); -    function Multiple(widget) { -      widget.$render = function() { -        var items = new HashMap(widget.$viewValue); -        forEach(selectElement.children(), function(option){ -          option.selected = isDefined(items.get(option.value)); +      forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) { +        widget.$watch('$' + name, function(value) { +          selectElement[value ? 'addClass' : 'removeClass']('ng-' + name);          }); -      }; +      }); + +      //////////////////////////// -      selectElement.bind('change', function() { -        widget.$apply(function() { -          var array = []; +      function Multiple(widget) { +        widget.$render = function() { +          var items = new HashMap(this.$viewValue);            forEach(selectElement.children(), function(option){ -            if (option.selected) { -              array.push(option.value); -            } +            option.selected = isDefined(items.get(option.value)); +          }); +        }; + +        selectElement.bind('change', function() { +          widget.$apply(function() { +            var array = []; +            forEach(selectElement.children(), function(option){ +              if (option.selected) { +                array.push(option.value); +              } +            }); +            widget.$emit('$viewChange', array);            }); -          widget.$emit('$viewChange', array);          }); -      }); -    } +      } -    function Single(widget) { -      widget.$render = function() { -        selectElement.val(widget.$viewValue); -      }; +      function Single(widget) { +        widget.$render = function() { +          selectElement.val(widget.$viewValue); +        }; -      selectElement.bind('change', function() { -        widget.$apply(function() { -          widget.$emit('$viewChange', selectElement.val()); +        selectElement.bind('change', function() { +          widget.$apply(function() { +            widget.$emit('$viewChange', selectElement.val()); +          });          }); -      }); - -      widget.$viewValue = selectElement.val(); -    } - -    function Options(widget) { -      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 + "'."); +        widget.$viewValue = selectElement.val();        } -      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)(modelScope); -        } -      }); -      selectElement.html(''); // clear contents +      function Options(widget) { +        var match; -      selectElement.bind('change', function() { -        widget.$apply(function() { -          var optionGroup, -              collection = valuesFn(modelScope) || [], -              key = selectElement.val(), -              tempScope = inherit(modelScope), -              value, optionElement, index, groupIndex, length, groupLength; +        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 + "'."); +        } -          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) { -                  if (keyName) tempScope[keyName] = key; -                  tempScope[valueName] = collection[optionElement.val()]; -                  value.push(valueFn(tempScope)); +        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)(modelScope); +          } +        }); +        selectElement.html(''); // clear contents + +        selectElement.bind('change', function() { +          widget.$apply(function() { +            var optionGroup, +                collection = valuesFn(modelScope) || [], +                key = selectElement.val(), +                tempScope = inherit(modelScope), +                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) { +                    if (keyName) tempScope[keyName] = key; +                    tempScope[valueName] = collection[optionElement.val()]; +                    value.push(valueFn(tempScope)); +                  }                  }                } -            } -          } else { -            if (key == '?') { -              value = undefined; -            } else if (key == ''){ -              value = null;              } else { -              tempScope[valueName] = collection[key]; -              if (keyName) tempScope[keyName] = key; -              value = valueFn(tempScope); +              if (key == '?') { +                value = undefined; +              } else if (key == ''){ +                value = null; +              } else { +                tempScope[valueName] = collection[key]; +                if (keyName) tempScope[keyName] = key; +                value = valueFn(tempScope); +              }              } -          } -          if (isDefined(value) && modelScope.$viewVal !== value) { -            widget.$emit('$viewChange', value); -          } +            if (isDefined(value) && modelScope.$viewVal !== value) { +              widget.$emit('$viewChange', value); +            } +          });          }); -      }); -      widget.$watch(render); -      widget.$render = render; - -      function render() { -        var optionGroups = {'':[]}, // Temporary location for the option groups before we render them -            optionGroupNames = [''], -            optionGroupName, -            optionGroup, -            option, -            existingParent, existingOptions, existingOption, -            modelValue = widget.$modelValue, -            values = valuesFn(modelScope) || [], -            keys = keyName ? sortedKeys(values) : values, -            groupLength, length, -            groupIndex, index, -            optionScope = inherit(modelScope), -            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; -        } +        widget.$watch(render); +        widget.$render = render; + +        function render() { +          var optionGroups = {'':[]}, // Temporary location for the option groups before we render them +              optionGroupNames = [''], +              optionGroupName, +              optionGroup, +              option, +              existingParent, existingOptions, existingOption, +              modelValue = widget.$modelValue, +              values = valuesFn(modelScope) || [], +              keys = keyName ? sortedKeys(values) : values, +              groupLength, length, +              groupIndex, index, +              optionScope = inherit(modelScope), +              selected, +              selectedSet = false, // nothing is selected yet +              lastElement, +              element; -        // We now build up the list of options we need (we merge later) -        for (index = 0; length = keys.length, index < length; index++) { -             optionScope[valueName] = values[keyName ? optionScope[keyName]=keys[index]:index]; -             optionGroupName = groupByFn(optionScope) || ''; -          if (!(optionGroup = optionGroups[optionGroupName])) { -            optionGroup = optionGroups[optionGroupName] = []; -            optionGroupNames.push(optionGroupName); -          }            if (multiple) { -            selected = selectedSet.remove(valueFn(optionScope)) != undefined; -          } else { -            selected = modelValue === valueFn(optionScope); -            selectedSet = selectedSet || selected; // see if at least one item is selected +            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;            } -          optionGroup.push({ -            id: keyName ? keys[index] : index,   // either the index into array or key from object -            label: displayFn(optionScope) || '', // 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); +          // We now build up the list of options we need (we merge later) +          for (index = 0; length = keys.length, index < length; index++) { +               optionScope[valueName] = values[keyName ? optionScope[keyName]=keys[index]:index]; +               optionGroupName = groupByFn(optionScope) || ''; +            if (!(optionGroup = optionGroups[optionGroupName])) { +              optionGroup = optionGroups[optionGroupName] = []; +              optionGroupNames.push(optionGroupName); +            } +            if (multiple) { +              selected = selectedSet.remove(valueFn(optionScope)) != undefined; +            } else { +              selected = modelValue === valueFn(optionScope); +              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(optionScope) || '', // 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});            } -          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)); -              } +          // 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 { -              // grow elements +              existingOptions = optionGroupsCache[groupIndex]; +              existingParent = existingOptions[0];  // either SELECT (no group) or OPTGROUP element -              // 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); +              // update the OPTGROUP label if not the same. +              if (existingParent.label != optionGroupName) { +                existingParent.element.attr('label', existingParent.label = optionGroupName);                } +            } -              existingOptions.push(existingOption = { -                  element: element, -                  label: option.label, -                  id: option.id, -                  selected: option.selected -              }); -              if (lastElement) { -                lastElement.after(element); +            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 { -                existingParent.element.append(element); +                // 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;                } -              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 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 { +    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());          } -        // remove any excessive OPTGROUPs from select -        while(optionGroupsCache.length > groupIndex) { -          optionGroupsCache.pop()[0].element.remove(); -        } -      }; +      }      } -  }]; -}); +  } +}]; | 
