From d8e08085de1189c01bae28a77a556d75be831015 Mon Sep 17 00:00:00 2001
From: vitalets <noginsk@rambler.ru>
Date: Sun, 13 Jan 2013 15:26:47 +0400
Subject: [PATCH] optgroups

---
 CHANGELOG.txt                            |  1 +
 src/editable-form/editable-form-utils.js | 29 +++++++++++-----
 src/inputs/list.js                       | 42 ++++++++++++++++--------
 src/inputs/select.js                     | 21 ++++++++----
 test/unit/select.js                      | 39 +++++++++++++++++++++-
 5 files changed, 101 insertions(+), 31 deletions(-)

diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index d4bbf7f..16ef758 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -4,6 +4,7 @@ X-editable changelog
 
 Version 1.4.1 wip
 ----------------------------
+[enh] select: support of OPTGROUP via `children` key in source (vitalets) 
 [enh] checklist: set checked via prop instead of attr (vitalets) 
 
 
diff --git a/src/editable-form/editable-form-utils.js b/src/editable-form/editable-form-utils.js
index af4689a..4eb6aee 100644
--- a/src/editable-form/editable-form-utils.js
+++ b/src/editable-form/editable-form-utils.js
@@ -136,17 +136,28 @@
            if(!sourceData || value === null) {
                return [];
            }
-           
-           //convert to array
-           if(!$.isArray(value)) {
-               value = [value];
-           }
                       
-           /*jslint eqeq: true*/           
-           var result = $.grep(sourceData, function(o){
-               return $.grep(value, function(v){ return v == o.value; }).length;
+           var isValArray = $.isArray(value),
+           result = [], 
+           that = this;
+
+           $.each(sourceData, function(i, o) {
+               if(o.children) {
+                   result = result.concat(that.itemsByValue(value, o.children));
+               } else {
+                   /*jslint eqeq: true*/
+                   if(isValArray) {
+                       if($.grep(value, function(v){ return v == o.value; }).length) {
+                           result.push(o); 
+                       }
+                   } else {
+                       if(value == o.value) {
+                           result.push(o); 
+                       }
+                   }
+                   /*jslint eqeq: false*/
+               }
            });
-           /*jslint eqeq: false*/
            
            return result;
        },
diff --git a/src/inputs/list.js b/src/inputs/list.js
index 7ecc469..0aab53d 100644
--- a/src/inputs/list.js
+++ b/src/inputs/list.js
@@ -200,35 +200,45 @@ List - abstract class for inputs that have source option loaded from js array or
         * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}]
         */
         makeArray: function(data) {
-            var count, obj, result = [], iterateEl;
+            var count, obj, result = [], item, iterateItem;
             if(!data || typeof data === 'string') {
                 return null; 
             }
 
             if($.isArray(data)) { //array
-                iterateEl = function (k, v) {
+                /* 
+                   function to iterate inside item of array if item is object.
+                   Caclulates count of keys in item and store in obj. 
+                */
+                iterateItem = function (k, v) {
                     obj = {value: k, text: v};
                     if(count++ >= 2) {
-                        return false;// exit each if object has more than one value
+                        return false;// exit from `each` if item has more than one key.
                     }
                 };
             
                 for(var i = 0; i < data.length; i++) {
-                    if(typeof data[i] === 'object') {
-                        count = 0;
-                        $.each(data[i], iterateEl);
-                        if(count === 1) {
+                    item = data[i]; 
+                    if(typeof item === 'object') {
+                        count = 0; //count of keys inside item
+                        $.each(item, iterateItem);
+                        //case: [{val1: 'text1'}, {val2: 'text2} ...]
+                        if(count === 1) { 
                             result.push(obj); 
-                        } else if(count > 1 && data[i].hasOwnProperty('value') && data[i].hasOwnProperty('text')) {
-                            result.push(data[i]);
-                        } else {
-                            //data contains incorrect objects
+                            //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...]
+                        } else if(count > 1) {
+                            //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text')
+                            if(item.children) {
+                                item.children = this.makeArray(item.children);   
+                            }
+                            result.push(item);
                         }
                     } else {
-                        result.push({value: data[i], text: data[i]}); 
+                        //case: ['text1', 'text2' ...]
+                        result.push({value: item, text: item}); 
                     }
                 }
-            } else {  //object
+            } else {  //case: {val1: 'text1', val2: 'text2, ...}
                 $.each(data, function (k, v) {
                     result.push({value: k, text: v});
                 });  
@@ -251,12 +261,16 @@ List - abstract class for inputs that have source option loaded from js array or
     List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
         /**
         Source data for list.  
-        If **array** - it should be in format: `[{value: 1, text: "text1"}, {...}]`  
+        If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`  
         For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order.
         
         If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option.
           
         If **function**, it should return data in format above (since 1.4.0).
+        
+        Since 1.4.1 key `children` supported to render OPTGROUPs (select input only).  
+        Example `[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]`. 
+
 		
         @property source 
         @type string | array | object | function
diff --git a/src/inputs/select.js b/src/inputs/select.js
index f08b512..0a0b4a7 100644
--- a/src/inputs/select.js
+++ b/src/inputs/select.js
@@ -31,14 +31,21 @@ $(function(){
     $.extend(Select.prototype, {
         renderList: function() {
             this.$input.empty();
-            
-            if(!$.isArray(this.sourceData)) {
-                return;
-            }
 
-            for(var i=0; i<this.sourceData.length; i++) {
-                this.$input.append($('<option>', {value: this.sourceData[i].value}).text(this.sourceData[i].text)); 
-            }
+            var fillItems = function($el, data) {
+                if($.isArray(data)) {
+                    for(var i=0; i<data.length; i++) {
+                        if(data[i].children) {
+                           $el.append(fillItems($('<optgroup>', {label: data[i].text}), data[i].children)); 
+                        } else {
+                           $el.append($('<option>', {value: data[i].value}).text(data[i].text)); 
+                        }
+                    }
+                }
+                return $el;
+            };        
+
+            fillItems(this.$input, this.sourceData);
             
             this.setClass();
             
diff --git a/test/unit/select.js b/test/unit/select.js
index d4f80d9..cb5cb20 100644
--- a/test/unit/select.js
+++ b/test/unit/select.js
@@ -689,6 +689,43 @@ $(function () {
             }, timeout);
             
         }, timeout);                                                  
-    });           
+    });  
+    
+    asyncTest("optgroup", function () {
+         var
+         selected = 2, 
+         e = $('<a href="#" data-type="select" data-value="'+selected+'" data-url="post.php"></a>').appendTo(fx).editable({
+             pk: 1,
+             source: [
+                 {text: 'groups', children: groups},
+                 {value: 'v1', text: 't1', children: ['a', 'b', 'c']},
+                 {value: 'v2', text: 't2'}
+             ]
+        });
+
+        equal(e.text(), groups[selected], 'text shown'); 
+                                  
+        e.click();
+        var p = tip(e);
+        ok(p.is(':visible'), 'container visible');
+        ok(p.find('select').length, 'select exists');
+        equal(p.find('select').find('option').length, size + 3 + 1, 'options loaded');
+        equal(p.find('select').val(), e.data('editable').value, 'selected value correct');
+
+        equal(p.find('select').find('optgroup').length, 2, 'optgroup loaded');
+        equal(p.find('select').find('optgroup').eq(0).children().length, size, 'optgroup items ok');
+        
+        selected = 'a';
+        p.find('select').val(selected);
+        p.find('form').submit(); 
+         
+         setTimeout(function() {
+               ok(!p.is(':visible'), 'popover closed')
+               equal(e.data('editable').value, selected, 'new value saved')
+               equal(e.text(), 'a', 'new text shown') 
+               e.remove();    
+               start();  
+         }, timeout);                              
+    });               
      
 });