Index: location.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/location/location.module,v
retrieving revision 1.222.2.25
diff -u -p -r1.222.2.25 location.module
--- location.module	30 Jul 2009 20:58:43 -0000	1.222.2.25
+++ location.module	18 Nov 2009 18:09:04 -0000
@@ -33,6 +33,12 @@ function location_menu() {
     'page callback' => '_location_autocomplete',
     'type' => MENU_CALLBACK,
   );
+  
+  $items['location/fetch_provinces'] = array(
+    'access arguments' => array('access content'),
+    'page callback' => '_location_fetch_provinces',
+    'type' => MENU_CALLBACK,
+    );
 
   $items['admin/settings/location'] = array(
     'title' => 'Location',
@@ -256,13 +262,31 @@ function _location_expand_location($elem
     if (!isset($element[$field])) {
       // @@@ Permission check hook?
       if ($fsettings[$field]['collect'] != 0) {
-        $element[$field] = location_invoke_locationapi($fdefaults[$field], 'field_expand', $field, $fsettings[$field]['collect'], $fdefaults);
+        $element[$field] = location_invoke_locationapi($fdefaults[$field], 'field_expand', $field, $fsettings[$field], $fdefaults);
         $element[$field]['#weight'] = (int)$fsettings[$field]['weight'];
       }
 
+      // If "State/Province" is using the select widget, update the element's options
+      if($field == 'province' && $fsettings[$field]['widget'] == 'select') {
+        $country = $element['#value']['country'];
+        $provinces = location_get_provinces($country);
+        // The submit handler expects to find the full province name, not the abbreviation.
+        // The select options should reflect this expectation.
+        if(count($provinces)) {
+          $element[$field]['#options'] = array_merge(array(
+            '' => t('Please select'), 
+            'xx' => t('NOT LISTED'), 
+            $provinces));
+        } else {
+        // If there are no provinces for the selected country, then we should default to n/a.
+        // TODO: This is going to cause problems if state/province is a required field.
+          $element[$field]['#options'] = array('' => t('n/a'));
+        }
+      }
+
       // Only include 'Street Additional' if 'Street' is 'allowed' or 'required'
       if ($field == 'street' && $fsettings[$field]['collect']) {
-        $element['additional'] = location_invoke_locationapi($defaults['additional'], 'field_expand', 'additional', 1, $defaults);
+        $element['additional'] = location_invoke_locationapi($defaults['additional'], 'field_expand', 'additional', $fsettings[$field], $defaults);
         $element['additional']['#weight'] = (int)$fsettings['additional']['weight'];
       }
     }
@@ -424,6 +448,8 @@ function _location_expand_location_setti
     4 => t('Force Default'), // Need to consider the new "defaults" when saving.
   );
 
+  $widgets = location_field_widgets();
+
   foreach ($fields as $field => $title) {
     $element[$field] = array(
       '#type' => 'fieldset',
@@ -438,7 +464,14 @@ function _location_expand_location_setti
       '#default_value' => $defaults[$field]['collect'],
       '#options' => $options,
     );
-
+    if(!empty($widgets[$field])) {
+      $element[$field]['widget'] = array(
+        '#type' => 'radios', 
+        '#default_value' => $defaults[$field]['widget'],
+        '#options' => $widgets[$field],
+      );
+    }
+      
     $temp = $defaults[$field]['default'];
     $element[$field]['default'] = location_invoke_locationapi($temp, 'field_expand', $field, 1, $defaults);
     $defaults[$field]['default'] = $temp;
@@ -459,6 +492,16 @@ function _location_expand_location_setti
   return $element;
 }
 
+function location_field_widgets() {
+  $widgets = array(
+    'province' => array(
+      'autocomplete' => 'Autodomplete',
+      'select' => 'Dropdown'
+      ),
+    );
+  return $widgets;
+}
+
 function theme_location_settings($element) {
   $rows = array();
   $header = array(
@@ -466,7 +509,7 @@ function theme_location_settings($elemen
       'data' => t('Name'),
       'colspan' => 2,
     ),
-    t('Collect'), t('Default'), t('Weight'));
+    t('Collect'), t('Widget'), t('Default'), t('Weight'));
 
   // Force country required.
   $element['country']['default']['#required'] = TRUE;
@@ -479,6 +522,11 @@ function theme_location_settings($elemen
     $row[] = array('data' => '', 'class' => 'location-settings-drag');
     $row[] = drupal_render($element[$key]['name']);
     $row[] = drupal_render($element[$key]['collect']);
+    if(!empty($element[$key]['widget'])) {
+      $row[] = drupal_render($element[$key]['widget']);
+    } else {
+      $row[] = '&nbsp;';
+    }
     $row[] = drupal_render($element[$key]['default']);
     $row[] = array('data' => drupal_render($element[$key]['weight']), 'class' => 'delta-order');
 
@@ -540,7 +588,7 @@ function location_locationapi(&$obj, $op
         'street'      => array('default' => '', 'collect' => 1, 'weight' => 4),
         'additional'  => array('default' => '', 'collect' => 1, 'weight' => 6),
         'city'        => array('default' => '', 'collect' => 0, 'weight' => 8),
-        'province'    => array('default' => '', 'collect' => 0, 'weight' => 10),
+        'province'    => array('default' => '', 'collect' => 0, 'weight' => 10, 'widget' => 'autocomplete'),
         'postal_code' => array('default' => '', 'collect' => 0, 'weight' => 12),
         'country'     => array('default' => variable_get('location_default_country', 'us'), 'collect' => 1, 'weight' => 14), // @@@ Fix weight?
         'locpick'     => array('default' => FALSE, 'collect' => 1, 'weight' => 20, 'nodiff' => TRUE),
@@ -583,6 +631,14 @@ function location_locationapi(&$obj, $op
       break;
 
     case 'field_expand':
+      if(is_array($a4)) {
+        $settings = $a4;
+      } else {
+        // on thie $op, $a4 is now expected to be an array, 
+        // but we make an exception for backwards compatibility.
+        $settings = array('default' => null, 'widget' => null, 
+                          'collect' => $a4, 'widget' => null);
+      }
       switch ($a3) {
         case 'name':
           return array(
@@ -593,7 +649,7 @@ function location_locationapi(&$obj, $op
             '#maxlength'      => 64,
             '#description'    => t('e.g. a place of business, venue, meeting point'),
             '#attributes'     => NULL,
-            '#required'       => ($a4 == 2),
+            '#required'       => ($settings['collect'] == 2),
           );
 
         case 'street':
@@ -603,7 +659,7 @@ function location_locationapi(&$obj, $op
             '#default_value'  => $obj,
             '#size'           => 64,
             '#maxlength'      => 64,
-            '#required'       => ($a4 == 2),
+            '#required'       => ($settings['collect'] == 2),
           );
 
         // Additional is linked to street.
@@ -626,28 +682,59 @@ function location_locationapi(&$obj, $op
             '#maxlength'      => 64,
             '#description'    => NULL,
             '#attributes'     => NULL,
-            '#required'       => ($a4 == 2),
+            '#required'       => ($settings['collect'] == 2),
           );
 
         case 'province':
+          $defaults = $a5;
           drupal_add_js(drupal_get_path('module', 'location') .'/location_autocomplete.js');
           $country = $a5['country'] ? $a5['country'] : variable_get('location_default_country', 'us');
-          return array(
-            '#type' => 'textfield',
-            '#title' => t('State/Province'),
-            '#autocomplete_path' => 'location/autocomplete/'. $country,
-            '#default_value' => $obj,
-            '#size' => 64,
-            '#maxlength' => 64,
-            '#description' => NULL,
-            // Used by province autocompletion js.
-            '#attributes' => array('class' => 'location_auto_province'),
-            '#required' => ($a4 == 2),
-          );
-
+          switch($settings['widget']) {
+            case 'select': {
+              static $js_set;
+              if(empty($js_set)) {
+                drupal_add_js(array(
+                  'location_fetch_provinces_url' => 'location/fetch_provinces',
+                  // TODO: Is there a javascript-y t() function to use instead?
+                  // If not, this should be standardized / modularized
+                  't_please_select' => t('Please Select'),
+                  't_not_listed' => t('NOT LISTED'),
+                  't_not_applicable' => t('n/a'),
+                ), 'setting');
+                $js_set = TRUE;
+              }
+              // Options are defined once during hook_element implementation
+              // @see _location_expand_location
+              // $options = array_merge(array('' => t('Please select'), 'xx' => t('NOT LISTED')), location_get_provinces($country));
+              return array(
+                '#type'           => 'select',
+                '#title'          => t('State/Province'),
+                '#default_value'  => $obj,
+                // '#options'        => $options,
+                '#description'    => NULL,
+                '#required'       => ($settings['collect'] == 2),
+                '#attributes'     => array('class' => 'location_dropdown_province'),
+              );
+              break;
+            }
+            default: {
+              return array(
+                '#type' => 'textfield',
+                '#title' => t('State/Province'),
+                '#autocomplete_path' => 'location/autocomplete/'. $country,
+                '#default_value' => $obj,
+                '#size' => 64,
+                '#maxlength' => 64,
+                '#description' => NULL,
+                '#attributes' => array('class' => 'location_auto_province'),
+                '#required' => ($settings['collect'] == 2),
+              );
+              break;
+            }
+          }
         case 'country':
           // Force default.
-          if ($a4 == 4) {
+          if ($settings['collect'] == 4) {
             return array(
               '#type' => 'value',
               '#value' => $obj,
@@ -661,7 +748,7 @@ function location_locationapi(&$obj, $op
               '#default_value'  => $obj,
               '#options'        => $options,
               '#description'    => NULL,
-              '#required'       => ($a4 == 2),
+              '#required'       => ($settings['collect'] == 2),
               // Used by province autocompletion js.
               '#attributes'     => array('class' => 'location_auto_country'),
             );
@@ -675,7 +762,7 @@ function location_locationapi(&$obj, $op
             '#default_value'  => $obj,
             '#size'           => 16,
             '#maxlength'      => 16,
-            '#required'       => ($a4 == 2),
+            '#required'       => ($settings['collect'] == 2),
           );
       }
       break;
@@ -938,6 +1025,18 @@ function _location_autocomplete($country
 }
 
 /**
+ * Ajax callback. Echo's a drupal_json'ed array of provinces for the given country
+ *
+ * @param 
+ *    String $country 
+ * @return 
+ *    void
+ */
+function _location_fetch_provinces($country) {
+  drupal_json(location_get_provinces($country));
+}
+
+/**
  * Epsilon test.
  * Helper function for seeing if two floats are equal.  We could use other functions, but all
  * of them belong to libraries that do not come standard with PHP out of the box.
Index: location_autocomplete.js
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/location/location_autocomplete.js,v
retrieving revision 1.3
diff -u -p -r1.3 location_autocomplete.js
--- location_autocomplete.js	2 Dec 2008 22:50:21 -0000	1.3
+++ location_autocomplete.js	18 Nov 2009 18:09:04 -0000
@@ -4,31 +4,123 @@
  * Twiddle the province autocomplete whenever the user changes the country.
  */
 Drupal.behaviors.location = function(context) {
+  $('select.location_dropdown_province option').each(function() {
+    if(!$(this).is('hidden')) {
+      var country_input = $('.location_auto_country', $(this).parents('fieldset:first, .views-exposed-form:first'));
+      var klass = 'location_dropdown_join_' + country_input.val();
+      if(!$(this).hasClass(klass)) {
+        $(this).addClass(klass);
+      }
+    }
+  });
   $('select.location_auto_country:not(.location-processed)', context).change(function(e) {
     var obj = this;
     var input = null;
     var result = this.className.match(/(location_auto_join_[^ ]*)/);
+    var type = null;
     if (result) {
-      input = $('.location_auto_province.' + result)
+      input = $('.location_auto_province.' + result);
+      type = 'autocomplete';
     }
     else {
       // No joining class found, fallback to searching the immediate area.
-      input = $('.location_auto_province', $(this).parents('fieldset:first, .views-exposed-form:first'))
+      input = $('.location_auto_province', $(this).parents('fieldset:first, .views-exposed-form:first'));
+      if(input && input.length) {
+        type = 'autocomplete';
+      } else {
+        input = $('.location_dropdown_province', $(this).parents('fieldset:first, .views-exposed-form:first'));
+        type = 'select';
+      }
     }
 
     if (input && input.length) {
-      //Unbind events on province field and empty its value
-      input.unbind().val('');
-      input.each(function(i) {
-        //Get the (hidden) *-autocomplete input element
-        var input_autocomplete = $('#' + this.id + '-autocomplete');
-        // Update autocomplete url
-        input_autocomplete.val(input_autocomplete.val().substr(0, input_autocomplete.val().lastIndexOf('/') + 1) + $(obj).val());
-        // Mark as not processed.
-        input_autocomplete.removeClass('autocomplete-processed');
-      });
-      // Reprocess.
-      Drupal.behaviors.autocomplete(document);
+      switch(type) {
+        case 'select':
+          // Update the select field with the options relevant for the selected country
+          // Hide all the current select options
+          // display:none; does nothing for <option> elements
+          if(!$('select#location_disabled_options')) {
+            $('body').append('<select id="location_disabled_options" style="display:none;visibility:hidden;"></select>');
+          }
+          // If the country's elements are already in the disabled select element, 
+          // no need to duplicate them.
+          var country_class = input.find('option').attr('class');
+          if(!$('select#location_disabled_options').find('option.'+country_class).length) {
+            // Otherwise, cache them in the disabled select element
+            input.find('option').each(function() { 
+              $('select#location_disabled_options').append(this);
+            });
+          }
+          input.find('option').remove();
+          input.append('<option>Loading...</option>');
+          
+          if(input.children('option.location_dropdown_join_' + $(obj).val()).length) {
+            // If we've already fetched options for the selected country, don't fetch again, just show the options
+            input.children('option.location_dropdown_join_' + $(obj).val()).show();
+          } else {
+            // If we don't have options for the selected country, update them via ajax
+            location_update_provinces($(obj).val(), input.attr('id'));
+          }
+          break;
+        default:
+          //Unbind events on province field and empty its value
+          input.unbind().val('');
+          input.each(function(i) {
+            //Get the (hidden) *-autocomplete input element
+            var input_autocomplete = $('#' + this.id + '-autocomplete');
+            // Update autocomplete url
+            input_autocomplete.val(input_autocomplete.val().substr(0, input_autocomplete.val().lastIndexOf('/') + 1) + $(obj).val());
+            console.log(input_autocomplete.val);
+            console.log($(obj).val());
+            // Mark as not processed.
+            input_autocomplete.removeClass('autocomplete-processed');
+          });
+          // Reprocess.
+          Drupal.behaviors.autocomplete(document);
+          break;
+      }
     }
   }).addClass('location-processed');
 };
+
+// Invoke the ajax request to fetch provinces for the specified country
+function location_update_provinces(country, input_id) {
+  if(!country) {
+    return;
+  } else {
+    return $.ajax({
+      url : Drupal.settings.basePath + Drupal.settings.location_fetch_provinces_url + '/' + country,
+      data : { input_id : input_id, country : country },
+      dataType : 'json',
+      success : location_update_provinces_callback,
+      error : function() { alert('Error in network connection. Please reload the page and try again (1).'); },
+    });
+  }
+}
+
+// On ajax request completion, update the appropriate select menu
+function location_update_provinces_callback(data, textStatus) {
+  var regexS = "[\\?&]input_id=([^&#]*)";
+  var regex = new RegExp( regexS );
+  var input_id = regex.exec(this.url);
+
+  regexS = "[\\?&]country=([^&#]*)";
+  regex = new RegExp( regexS );
+  var country = regex.exec(this.url);
+  
+  if(!input_id || input_id.length < 2 || !$('#' + input_id[1]).length) {
+    alert('Error in network connection. Please reload the page and try again (2).');
+    return;
+  }
+  $('#' + input_id[1]).find('option').remove();
+  console.log($(data));
+  if($(data).length) {
+    $('#' + input_id[1]).append('<option value="">' + Drupal.settings.t_please_select + '</option>');
+    $('#' + input_id[1]).append('<option value="xx">' + Drupal.settings.t_not_listed + '</option>');
+    $.each(data, function(key, value) {
+      $('#' + input_id[1]).append('<option value="' + key + '" class="location_dropdown_join_' + country + '">' + value + '</option>');
+    });
+  } else {
+    $('#' + input_id[1]).append('<option value="">' + Drupal.settings.t_not_applicable + '</option>');
+  }
+}
