@@ -598,21 +598,27 @@ angularWidget('button', inputWidgetSelector);
598
598
* @element select
599
599
* @param {comprehension_expression } comprehension in following form
600
600
*
601
- * * _select_ `for` _value_ `in` _array_
601
+ * * _label_ `for` _value_ `in` _array_
602
602
* * _select_ `as` _label_ `for` _value_ `in` _array_
603
- * * _select_ `for` `(`_key_`,` _value_`)` `in` _object_
603
+ * * _select_ `as` _label_ `group by` _group_ `for` _value_ `in` _array_
604
+ * * _select_ `group by` _group_ `for` _value_ `in` _array_
605
+ * * _label_ `for` `(`_key_`,` _value_`)` `in` _object_
604
606
* * _select_ `as` _label_ `for` `(`_key_`,` _value_`)` `in` _object_
607
+ * * _select_ `as` _label_ `group by` _group_ `for` `(`_key_`,` _value_`)` `in` _object_
608
+ * * _select_ `group by` _group_ `for` `(`_key_`,` _value_`)` `in` _object_
605
609
*
606
610
* Where:
607
611
*
608
612
* * _array_ / _object_: an expression which evaluates to an array / object to iterate over.
609
- * * _value_: local variable which will reffer to the item in the _array_ or _object_ during
610
- * iteration
611
- * * _key_: local variable which will refer to the key in the _object_ during the iteration
612
- * * _select_: The result of this expression will be assigned to the scope.
613
- * The _select_ can be ommited, in which case the _item_ itself will be assigned.
613
+ * * _value_: local variable which will refer to each item in the _array_ or each value of
614
+ * _object_ during itteration.
615
+ * * _key_: local variable which will refer to the key in the _object_ during the iteration.
614
616
* * _label_: The result of this expression will be the `option` label. The
615
- * `expression` most likely refers to the _item_ variable. (optional)
617
+ * `expression` will most likely refer to the _value_ variable.
618
+ * * _select_: The result of this expression will be bound to the scope. If not specified,
619
+ * _select_ expression will default to _value_.
620
+ * * _group_: The result of this expression will be used to group options using the `optgroup`
621
+ * DOM element.
616
622
*
617
623
* @example
618
624
<doc:example>
@@ -667,8 +673,8 @@ angularWidget('button', inputWidgetSelector);
667
673
</doc:scenario>
668
674
</doc:example>
669
675
*/
670
- // 000012222111111111133330000000004555555555555555554666666777777777777777776666666888888888888888888888864000000009999
671
- var NG_OPTIONS_REGEXP = / ^ \s * ( ( . * ) \s + a s \s + ) ? ( .* ) \s + f o r \s + ( ( [ \$ \w ] [ \$ \w \d ] * ) | ( \( \s * ( [ \$ \w ] [ \$ \w \d ] * ) \s * , \s * ( [ \$ \w ] [ \$ \w \d ] * ) \s * \) ) ) \s + i n \s + ( .* ) $ / ;
676
+ // 00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777
677
+ var NG_OPTIONS_REGEXP = / ^ \s * ( . * ? ) (?: \s + a s \s + ( . * ? ) ) ? (?: \s + g r o u p \s + b y \s + ( .* ) ) ? \s + f o r \s + (?: ( [ \$ \w ] [ \$ \w \d ] * ) | (?: \( \s * ( [ \$ \w ] [ \$ \w \d ] * ) \s * , \s * ( [ \$ \w ] [ \$ \w \d ] * ) \s * \) ) ) \s + i n \s + ( .* ) $ / ;
672
678
angularWidget ( 'select' , function ( element ) {
673
679
this . descend ( true ) ;
674
680
this . directives ( true ) ;
@@ -684,53 +690,71 @@ angularWidget('select', function(element){
684
690
"Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '" +
685
691
expression + "'." ) ;
686
692
}
687
- var displayFn = expressionCompile ( match [ 3 ] ) . fnSelf ;
688
- var valueName = match [ 5 ] || match [ 8 ] ;
689
- var keyName = match [ 7 ] ;
690
- var valueFn = expressionCompile ( match [ 2 ] || valueName ) . fnSelf ;
691
- var valuesFn = expressionCompile ( match [ 9 ] ) . fnSelf ;
693
+ var displayFn = expressionCompile ( match [ 2 ] || match [ 1 ] ) . fnSelf ;
694
+ var valueName = match [ 4 ] || match [ 6 ] ;
695
+ var keyName = match [ 5 ] ;
696
+ var groupByFn = expressionCompile ( match [ 3 ] || '' ) . fnSelf ;
697
+ var valueFn = expressionCompile ( match [ 2 ] ? match [ 1 ] : valueName ) . fnSelf ;
698
+ var valuesFn = expressionCompile ( match [ 7 ] ) . fnSelf ;
692
699
// we can't just jqLite('<option>') since jqLite is not smart enough
693
700
// to create it in <select> and IE barfs otherwise.
694
- var option = jqLite ( document . createElement ( 'option' ) ) ;
695
- return function ( select ) {
701
+ var optionTemplate = jqLite ( document . createElement ( 'option' ) ) ;
702
+ var optGroupTemplate = jqLite ( document . createElement ( 'optgroup' ) ) ;
703
+ var nullOption = false ; // if false then user will not be able to select it
704
+ return function ( selectElement ) {
696
705
var scope = this ;
697
- var optionElements = [ ] ;
698
- var optionTexts = [ ] ;
699
- var lastSelectValue = isMultiselect ? { } : false ;
700
- var nullOption = option . clone ( ) . val ( '' ) ;
701
- var missingOption = option . clone ( ) . val ( '?' ) ;
706
+
707
+ // This is an array of array of existing option groups in DOM. We try to reuse these if possible
708
+ // optionGroupsCache[0] is the options with no option group
709
+ // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
710
+ var optionGroupsCache = [ [ { element : selectElement , label : '' } ] ] ;
702
711
var model = modelAccessor ( scope , element ) ;
703
712
704
713
// find existing special options
705
- forEach ( select . children ( ) , function ( option ) {
706
- if ( option . value == '' ) nullOption = false ;
714
+ forEach ( selectElement . children ( ) , function ( option ) {
715
+ if ( option . value == '' )
716
+ // User is allowed to select the null.
717
+ nullOption = { label :jqLite ( option ) . text ( ) , id :'' } ;
707
718
} ) ;
719
+ selectElement . html ( '' ) ; // clear contents
708
720
709
- select . bind ( 'change' , function ( ) {
721
+ selectElement . bind ( 'change' , function ( ) {
722
+ var optionGroup ;
710
723
var collection = valuesFn ( scope ) || [ ] ;
711
- var value = select . val ( ) ;
712
- var index , length ;
724
+ var key = selectElement . val ( ) ;
725
+ var value ;
726
+ var optionElement ;
727
+ var index , groupIndex , length , groupLength ;
713
728
var tempScope = scope . $new ( ) ;
714
729
try {
715
730
if ( isMultiselect ) {
716
731
value = [ ] ;
717
- for ( index = 0 , length = optionElements . length ; index < length ; index ++ ) {
718
- if ( optionElements [ index ] [ 0 ] . selected ) {
719
- tempScope [ valueName ] = collection [ index ] ;
720
- value . push ( valueFn ( tempScope ) ) ;
732
+ for ( groupIndex = 0 , groupLength = optionGroupsCache . length ;
733
+ groupIndex < groupLength ;
734
+ groupIndex ++ ) {
735
+ // list of options for that group. (first item has the parent)
736
+ optionGroup = optionGroupsCache [ groupIndex ] ;
737
+
738
+ for ( index = 1 , length = optionGroup . length ; index < length ; index ++ ) {
739
+ if ( ( optionElement = optionGroup [ index ] . element ) [ 0 ] . selected ) {
740
+ if ( keyName ) tempScope [ keyName ] = key ;
741
+ tempScope [ valueName ] = collection [ optionElement . val ( ) ] ;
742
+ value . push ( valueFn ( tempScope ) ) ;
743
+ }
721
744
}
722
745
}
723
746
} else {
724
- if ( value == '?' ) {
747
+ if ( key == '?' ) {
725
748
value = undefined ;
726
- } else if ( value == '' ) {
749
+ } else if ( key == '' ) {
727
750
value = null ;
728
751
} else {
729
- tempScope [ valueName ] = collection [ value ] ;
752
+ tempScope [ valueName ] = collection [ key ] ;
753
+ if ( keyName ) tempScope [ keyName ] = key ;
730
754
value = valueFn ( tempScope ) ;
731
755
}
732
756
}
733
- if ( ! isUndefined ( value ) && model . get ( ) !== value ) {
757
+ if ( isDefined ( value ) && model . get ( ) !== value ) {
734
758
onChange ( scope ) ;
735
759
model . set ( value ) ;
736
760
}
@@ -744,32 +768,46 @@ angularWidget('select', function(element){
744
768
745
769
scope . $onEval ( function ( ) {
746
770
var scope = this ;
771
+
772
+ // Temporary location for the option groups before we render them
773
+ var optionGroups = {
774
+ '' :[ ]
775
+ } ;
776
+ var optionGroupNames = [ '' ] ;
777
+ var optionGroupName ;
778
+ var optionGroup ;
779
+ var option ;
780
+ var existingParent , existingOptions , existingOption ;
747
781
var values = valuesFn ( scope ) || [ ] ;
748
782
var keys = values ;
749
783
var key ;
750
- var value ;
751
- var length ;
784
+ var groupLength , length ;
752
785
var fragment ;
753
- var index ;
754
- var optionText ;
786
+ var groupIndex , index ;
755
787
var optionElement ;
756
788
var optionScope = scope . $new ( ) ;
757
789
var modelValue = model . get ( ) ;
758
- var currentItem ;
759
- var selectValue = '' ;
790
+ var selected ;
791
+ var selectedSet = false ; // nothing is selected yet
760
792
var isMulti = isMultiselect ;
793
+ var lastElement ;
794
+ var element ;
761
795
762
796
try {
763
797
if ( isMulti ) {
764
- selectValue = new HashMap ( ) ;
798
+ selectedSet = new HashMap ( ) ;
765
799
if ( modelValue && isNumber ( length = modelValue . length ) ) {
766
800
for ( index = 0 ; index < length ; index ++ ) {
767
- selectValue . put ( modelValue [ index ] , true ) ;
801
+ selectedSet . put ( modelValue [ index ] , true ) ;
768
802
}
769
803
}
804
+ } else if ( modelValue === null || nullOption ) {
805
+ // if we are not multiselect, and we are null then we have to add the nullOption
806
+ optionGroups [ '' ] . push ( extend ( { selected :modelValue === null , id :'' , label :'' } , nullOption ) ) ;
807
+ selectedSet = true ;
770
808
}
771
809
772
- // If we have a keyName then we are itterating over on object. We
810
+ // If we have a keyName then we are iterating over on object. We
773
811
// grab the keys and sort them.
774
812
if ( keyName ) {
775
813
keys = [ ] ;
@@ -780,68 +818,111 @@ angularWidget('select', function(element){
780
818
keys . sort ( ) ;
781
819
}
782
820
821
+ // We now build up the list of options we need (we merge later)
783
822
for ( index = 0 ; length = keys . length , index < length ; index ++ ) {
784
823
optionScope [ valueName ] = values [ keyName ? optionScope [ keyName ] = keys [ index ] :index ] ;
785
- currentItem = valueFn ( optionScope ) ;
786
- optionText = displayFn ( optionScope ) ;
787
- if ( optionTexts . length > index ) {
788
- // reuse
789
- optionElement = optionElements [ index ] ;
790
- if ( optionText != optionTexts [ index ] ) {
791
- ( optionElement ) . text ( optionTexts [ index ] = optionText ) ;
792
- }
793
- } else {
794
- // grow
795
- if ( ! fragment ) {
796
- fragment = document . createDocumentFragment ( ) ;
797
- }
798
- optionTexts . push ( optionText ) ;
799
- optionElements . push ( optionElement = option . clone ( ) ) ;
800
- optionElement . attr ( 'value' , index ) . text ( optionText ) ;
801
- fragment . appendChild ( optionElement [ 0 ] ) ;
824
+ optionGroupName = groupByFn ( optionScope ) || '' ;
825
+ if ( ! ( optionGroup = optionGroups [ optionGroupName ] ) ) {
826
+ optionGroup = optionGroups [ optionGroupName ] = [ ] ;
827
+ optionGroupNames . push ( optionGroupName ) ;
802
828
}
803
829
if ( isMulti ) {
804
- if ( lastSelectValue [ index ] != ( value = selectValue . remove ( currentItem ) ) ) {
805
- optionElement [ 0 ] . selected = ! ! ( lastSelectValue [ index ] = value ) ;
806
- }
830
+ selected = ! ! selectedSet . remove ( valueFn ( optionScope ) ) ;
807
831
} else {
808
- if ( modelValue == currentItem ) {
809
- selectValue = index ;
810
- }
832
+ selected = modelValue === valueFn ( optionScope ) ;
833
+ selectedSet = selectedSet || selected ; // see if at least one item is selected
811
834
}
835
+ optionGroup . push ( {
836
+ id : keyName ? keys [ index ] : index , // either the index into array or key from object
837
+ label : displayFn ( optionScope ) || '' , // what will be seen by the user
838
+ selected : selected // determine if we should be selected
839
+ } ) ;
812
840
}
813
- if ( fragment ) {
814
- select . append ( jqLite ( fragment ) ) ;
815
- }
816
- // shrink children
817
- while ( optionElements . length > index ) {
818
- optionElements . pop ( ) . remove ( ) ;
819
- optionTexts . pop ( ) ;
820
- delete lastSelectValue [ optionElements . length ] ;
841
+ optionGroupNames . sort ( ) ;
842
+ if ( ! isMulti && ! selectedSet ) {
843
+ // nothing was selected, we have to insert the undefined item
844
+ optionGroups [ '' ] . unshift ( { id :'?' , label :'' , selected :true } ) ;
821
845
}
822
846
823
- if ( ! isMulti ) {
824
- if ( selectValue === '' && modelValue ) {
825
- // We could not find a match
826
- selectValue = '?' ;
827
- }
847
+ // Now we need to update the list of DOM nodes to match the optionGroups we computed above
848
+ for ( groupIndex = 0 , groupLength = optionGroupNames . length ;
849
+ groupIndex < groupLength ;
850
+ groupIndex ++ ) {
851
+ // current option group name or '' if no group
852
+ optionGroupName = optionGroupNames [ groupIndex ] ;
853
+
854
+ // list of options for that group. (first item has the parent)
855
+ optionGroup = optionGroups [ optionGroupName ] ;
856
+
857
+ if ( optionGroupsCache . length <= groupIndex ) {
858
+ // we need to grow the optionGroups
859
+ optionGroupsCache . push (
860
+ existingOptions = [
861
+ existingParent = {
862
+ element : optGroupTemplate . clone ( ) . attr ( 'label' , optionGroupName ) ,
863
+ label : optionGroup . label
864
+ }
865
+ ]
866
+ ) ;
867
+ selectElement . append ( existingParent . element ) ;
868
+ } else {
869
+ existingOptions = optionGroupsCache [ groupIndex ] ;
870
+ existingParent = existingOptions [ 0 ] ; // either SELECT (no group) or OPTGROUP element
828
871
829
- // update the selected item
830
- if ( lastSelectValue !== selectValue ) {
831
- if ( nullOption ) {
832
- if ( lastSelectValue == '' ) nullOption . remove ( ) ;
833
- if ( selectValue === '' ) select . prepend ( nullOption ) ;
872
+ // update the OPTGROUP label if not the same.
873
+ if ( existingParent . label != optionGroupName ) {
874
+ existingParent . element . attr ( 'label' , existingParent . label = optionGroupName ) ;
834
875
}
876
+ }
835
877
836
- if ( missingOption ) {
837
- if ( lastSelectValue == '?' ) missingOption . remove ( ) ;
838
- if ( selectValue === '?' ) select . prepend ( missingOption ) ;
878
+ lastElement = null ; // start at the begining
879
+ for ( index = 0 , length = optionGroup . length ; index < length ; index ++ ) {
880
+ option = optionGroup [ index ] ;
881
+ if ( existingOption = existingOptions [ index + 1 ] ) {
882
+ // reuse elements
883
+ lastElement = existingOption . element ;
884
+ if ( existingOption . label !== option . label ) {
885
+ lastElement . text ( existingOption . label = option . label ) ;
886
+ }
887
+ if ( existingOption . id !== option . id ) {
888
+ lastElement . val ( existingOption . id = option . id ) ;
889
+ }
890
+ if ( existingOption . selected !== option . selected ) {
891
+ lastElement . attr ( 'selected' , option . selected ) ;
892
+ }
893
+ } else {
894
+ // grow elements
895
+ // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
896
+ // in this version of jQuery on some browser the .text() returns a string
897
+ // rather then the element.
898
+ ( element = optionTemplate . clone ( ) )
899
+ . val ( option . id )
900
+ . attr ( 'selected' , option . selected )
901
+ . text ( option . label ) ;
902
+ existingOptions . push ( existingOption = {
903
+ element : element ,
904
+ label : option . label ,
905
+ id : option . id ,
906
+ checked : option . selected
907
+ } ) ;
908
+ if ( lastElement ) {
909
+ lastElement . after ( element ) ;
910
+ } else {
911
+ existingParent . element . append ( element ) ;
912
+ }
913
+ lastElement = element ;
839
914
}
840
-
841
- select . val ( lastSelectValue = selectValue ) ;
915
+ }
916
+ // remove any excessive OPTIONs in a group
917
+ index ++ ; // increment since the existingOptions[0] is parent element not OPTION
918
+ while ( existingOptions . length > index ) {
919
+ existingOptions . pop ( ) . element . remove ( ) ;
842
920
}
843
921
}
844
-
922
+ // remove any excessive OPTGROUPs from select
923
+ while ( optionGroupsCache . length > groupIndex ) {
924
+ optionGroupsCache . pop ( ) [ 0 ] . element . remove ( ) ;
925
+ }
845
926
} finally {
846
927
optionScope = null ; // TODO(misko): needs to be $destroy()
847
928
}
0 commit comments