TRUE, ); return $plugins; } /** * Implements hook_views_api(). */ function addressfield_views_api() { return array( 'api' => 3, 'path' => drupal_get_path('module', 'addressfield') . '/views', ); } /** * Implements hook_module_implements_alter(). * * Moves the hook_token_info_alter() implementation to the bottom so it is * invoked after all modules implementing the same hook. */ function addressfield_module_implements_alter(&$implementations, $hook) { if ($hook == 'token_info_alter') { // Make sure that the $implementations list is populated before altering it, // to work around a crash experienced by some people (#2181001). if (isset($implementations['addressfield'])) { $group = $implementations['addressfield']; unset($implementations['addressfield']); $implementations['addressfield'] = $group; } } } /** * Returns a list of address fields optionally filtered by entity type. * * @param string $entity_type * Optional machine-name of an entity type to filter the returned array by. * * @return array * An array of address field mapping data. */ function addressfield_get_address_fields($entity_type = '') { $fields = &drupal_static(__FUNCTION__ . '_' . $entity_type); if (isset($fields)) { return $fields; } // Get mapping data for all address fields. $fields = array_filter(field_info_field_map(), 'addressfield_field_map_filter'); // Filter the list of fields by entity type if specified. if (!empty($fields) && !empty($entity_type)) { foreach ($fields as $field_name => $field) { if (!isset($field['bundles'][$entity_type])) { unset($fields[$field_name]); } } } return $fields; } /** * Returns TRUE if a field map array value represents an addressfield. * * Provided for use as a callback by array_filter(). */ function addressfield_field_map_filter($field) { return !empty($field['type']) && $field['type'] == 'addressfield'; } /** * Get the list of format plugins. */ function addressfield_format_plugins() { ctools_include('plugins'); $plugins = ctools_get_plugins('addressfield', 'format'); uasort($plugins, 'ctools_plugin_sort'); return $plugins; } /** * Get the list of format plugins in a format suitable for #options. */ function addressfield_format_plugins_options() { $options = array(); foreach (addressfield_format_plugins() as $widget => $info) { $options[$widget] = check_plain($info['title']); } return $options; } /** * @defgroup addressfield_format Address format API * @{ * API for generating address forms and display formats. * * Addresses forms and display formats are collaboratively generated by one or * more format handler plugins. An address with a name and a company, for example, * will be generated by three handlers: * - 'address' that will generate the country, locality, street blocks * - 'organisation' that will add the organisation block to the address * - 'name-full' that will add a first name and last name block to the address * * A format handler is a CTools plugin of type 'addressfield' / 'format'. Each * handler is passed the format in turn, and can add to or modify the format. * * The format itself is a renderable array stub. This stub will be transformed * into either a Form API array suitable for use as part of a form or into a * renderable array suitable for use with drupal_render(). The following * modifications are done: * - when rendering as a form, every element which name (its key in the array) * is a valid addressfield column (see addressfield_field_schema()), will * be transformed into a form element, either using a type explicitly * defined in '#widget_type' or using 'select' if '#options' is set or * 'textfield' if it is not. In addition, the '#default_value' of every * field will be populated from the address being edited. * - when rendering as a formatter, every element which name (its key in the array) * is a valid addressfield column (see addressfield_field_schema()), will * be transformed into a renderable element, either using a type explicitly * defined in '#render_type' or else using 'addressfield_container'. When * the type is 'addressfield_container' the element will be rendered as * an HTML element set by '#tag' (default: span). */ /** * Generate a format for a given address. * * @param $address * The address format being generated. * @param $handlers * The format handlers to use to generate the format. * @param $context * An associative array of context information pertaining to how the address * format should be generated. If no mode is given, it will initialize to the * default value. The remaining context keys should only be present when the * address format is being generated for a field: * - mode: either 'form' or 'render'; defaults to 'render'. * - field: the field info array. * - instance: the field instance array. * - langcode: the langcode of the language the field is being rendered in. * - delta: the delta value of the given address. * * @return * A renderable array suitable for use as part of a form (if 'mode' is 'form') * or for formatted address output when passed to drupal_render(). */ function addressfield_generate($address, array $handlers, array $context = array()) { // If no mode is given in the context array, default it to 'render'. if (empty($context['mode'])) { $context['mode'] = 'render'; } ctools_include('plugins'); $format = array(); // Add the handlers, ordered by weight. $plugins = addressfield_format_plugins(); $format['#handlers'] = array_intersect(array_keys($plugins), $handlers); foreach ($format['#handlers'] as $handler) { if ($callback = ctools_plugin_load_function('addressfield', 'format', $handler, 'format callback')) { $callback($format, $address, $context); } } // Store the address in the format, for processing. $format['#address'] = $address; // Post-process the format stub, depending on the rendering mode. if ($context['mode'] == 'form') { $format['#addressfield'] = TRUE; $format['#process'][] = 'addressfield_process_format_form'; } elseif ($context['mode'] == 'render') { $format['#pre_render'][] = 'addressfield_render_address'; } return $format; } /** * Generate a full-fledged form from a format snippet, as returned by addressfield_formats(). */ function addressfield_process_format_form($format, &$form_state, $complete_form) { // Make sure to load all the plugins that participated in this format. ctools_include('plugins'); foreach ($format['#handlers'] as $handler) { ctools_plugin_load_function('addressfield', 'format', $handler, 'format callback'); } _addressfield_process_format_form($format, $format['#address']); return $format; } function _addressfield_process_format_form(&$format, $address) { foreach (element_children($format) as $key) { $child = &$format[$key]; // Automatically convert any element in the format array to an appropriate // form element that matches one of the address component names. if (in_array($key, array('name_line', 'first_name', 'last_name', 'organisation_name', 'country', 'administrative_area', 'sub_administrative_area', 'locality', 'dependent_locality', 'postal_code', 'thoroughfare', 'premise', 'sub_premise'))) { // Set the form element type for the address component to whatever the // address format specified in its #widget_type property. if (isset($child['#widget_type'])) { $child['#type'] = $child['#widget_type']; } else { // If the element didn't specify a #widget_type and has options, turn it // into a select list and unset its #size value, which is typically used // to provide the width of a textfield. if (isset($child['#options'])) { $child['#type'] = 'select'; unset($child['#size']); } else { // Otherwise go ahead and make it a textfield. $child['#type'] = 'textfield'; } } if (isset($address[$key])) { $child['#default_value'] = $address[$key]; } } // Recurse through the element's children if it has any. _addressfield_process_format_form($child, $address); } } /** * Render an address in a given format. */ function addressfield_render_address($format) { _addressfield_render_address($format, $format['#address']); return $format; } function _addressfield_render_address(&$format, $address) { foreach (element_children($format) as $key) { $child = &$format[$key]; // Automatically expand elements that match one of the fields of the address // structure. if (in_array($key, array('name_line', 'first_name', 'last_name', 'organisation_name', 'country', 'administrative_area', 'sub_administrative_area', 'locality', 'dependent_locality', 'postal_code', 'thoroughfare', 'premise', 'sub_premise'), TRUE)) { if (isset($child['#render_type'])) { $child['#type'] = $child['#render_type']; } else { $child['#type'] = 'addressfield_container'; if (!isset($child['#tag'])) { $child['#tag'] = 'span'; } } // If the element instructs us to render the option value instead of the // raw address element value and its #options array has a matching key, // swap it out for the option value now. if (!empty($child['#render_option_value']) && isset($address[$key]) && isset($child['#options'][$address[$key]])) { $child['#children'] = check_plain($child['#options'][$address[$key]]); } elseif (isset($address[$key])) { $child['#children'] = check_plain($address[$key]); } else { $child['#children'] = ''; } // Skip empty elements. if ((string) $child['#children'] === '') { $child['#access'] = FALSE; } // Add #field_prefix and #field_suffix to the prefixes and suffixes. if (isset($child['#field_prefix'])) { $child['#prefix'] = (isset($child['#prefix']) ? $child['#prefix'] : '') . $child['#field_prefix']; } if (isset($child['#field_suffix'])) { $child['#suffix'] = (isset($child['#suffix']) ? $child['#suffix'] : '') . $child['#field_suffix']; } } // Recurse through the child. _addressfield_render_address($child, $address); } } /** * @} End of "ingroup addressfield_format" */ /** * Implementation of hook_theme(). */ function addressfield_theme() { $hooks['addressfield_container'] = array( 'render element' => 'element', ); return $hooks; } /** * Render a container for a set of address fields. */ function theme_addressfield_container($variables) { $element = $variables['element']; $element['#children'] = trim($element['#children']); // Remove the autocomplete attribute because the W3C validator complains. // It's only used on forms anyway. unset($element['#attributes']['autocomplete']); if (strlen($element['#children']) > 0) { $output = '<' . $element['#tag'] . drupal_attributes($element['#attributes']) . '>'; $output .= $element['#children']; $output .= ''; // Add a linebreak to the HTML after a div. This is invisible on the // rendered page but improves the appearance of address field output when // HTML tags are stripped, such as by Views Data Export. if ($element['#tag'] == 'div') { $output .= PHP_EOL; } return $output; } else { return ''; } } /** * Implementation of hook_element_info(). */ function addressfield_element_info() { $types['addressfield_container'] = array( '#theme_wrappers' => array('addressfield_container'), '#process' => array('addressfield_widget_process'), '#attributes' => array(), '#tag' => 'div', ); return $types; } /** * Form API process function: set the #parents of the children of this element so they appear at the same level as the parent. */ function addressfield_widget_process($element) { foreach (element_children($element) as $key) { $element[$key]['#parents'] = $element['#parents']; $element[$key]['#parents'][count($element[$key]['#parents']) - 1] = $key; } return $element; } /** * Implements hook_field_info() */ function addressfield_field_info() { $fields = array(); $fields['addressfield'] = array( 'label' => t('Postal address'), 'description' => t('A field type used for storing postal addresses according the xNAL standard.'), 'settings' => array(), 'instance_settings' => array(), 'default_widget' => 'addressfield_standard', 'default_formatter' => 'addressfield_default', 'property_type' => 'addressfield', 'property_callbacks' => array('addressfield_property_info_callback'), ); return $fields; } /** * Returns an array of default values for the addressfield form elements. * * @param $field * The field array. * @param $instance * The instance array. * @param $address * The current address values, if known. Allows for per-country defaults. * * @return * An array of default values. */ function addressfield_default_values($field, $instance, array $address = array()) { $available_countries = _addressfield_country_options_list($field, $instance); $default_country = $instance['widget']['settings']['default_country']; // Resolve the special site_default option. if ($default_country == 'site_default') { $default_country = variable_get('site_default_country', ''); } // Fallback to the first country in the list if the default country is not // available, or is empty even though the field is required. $not_available = $default_country && !isset($available_countries[$default_country]); $empty_but_required = empty($default_country) && !empty($instance['required']); if ($not_available || $empty_but_required) { $default_country = key($available_countries); } $default_values = array( 'country' => $default_country, 'name_line' => '', 'first_name' => '', 'last_name' => '', 'organisation_name' => '', 'administrative_area' => '', 'sub_administrative_area' => '', 'locality' => '', 'dependent_locality' => '', 'postal_code' => '', 'thoroughfare' => '', 'premise' => '', 'sub_premise' => '', 'data' => '', ); // Allow other modules to alter the default values. $context = array( 'field' => $field, 'instance' => $instance, 'address' => $address, ); drupal_alter('addressfield_default_values', $default_values, $context); return $default_values; } /** * Implements hook_field_is_empty(). */ function addressfield_field_is_empty($item, $field) { // Every address field must have at least a country value or it is considered // empty, even if it has name information. return empty($item['country']); } /** * Implements hook_field_presave(). */ function addressfield_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) { foreach ($items as $delta => &$item) { // If the first name and last name are set but the name line isn't... if (isset($item['first_name']) && isset($item['last_name']) && !isset($item['name_line'])) { // Combine the first and last name to be the name line. $items[$delta]['name_line'] = $items[$delta]['first_name'] . ' ' . $items[$delta]['last_name']; } elseif (isset($item['name_line'])) { // Otherwise if the name line is set, separate it out into a best guess at // the first and last name. $names = explode(' ', $item['name_line']); $item['first_name'] = array_shift($names); $item['last_name'] = implode(' ', $names); } // Trim whitespace from all of the address components and convert any double // spaces to single spaces. foreach ($item as $key => &$value) { if (!in_array($key, array('data')) && is_string($value)) { $value = trim(preg_replace('/[[:blank:]]{2,}/u', ' ', $value)); } } } } /** * Implements hook_field_widget_info() */ function addressfield_field_widget_info() { $widgets = array(); $widgets['addressfield_standard'] = array( 'label' => t('Dynamic address form'), 'field types' => array('addressfield'), 'settings' => array( 'available_countries' => array(), // Can't use variable_get('site_default_country') here because it would // set the value in stone. Instead, the site_default option allows the // default country to always reflect the current site setting. 'default_country' => 'site_default', 'format_handlers' => array('address'), ), ); return $widgets; } /** * Implements hook_field_widget_settings_form() */ function addressfield_field_widget_settings_form($field, $instance) { $widget = $instance['widget']; $defaults = field_info_widget_settings($widget['type']); $settings = array_merge($defaults, $widget['settings']); $form = array(); if ($widget['type'] == 'addressfield_standard') { $form['available_countries'] = array( '#type' => 'select', '#multiple' => TRUE, '#title' => t('Available countries'), '#description' => t('If no countries are selected, all countries will be available.'), '#options' => _addressfield_country_options_list(), '#default_value' => $settings['available_countries'], ); $form['default_country'] = array( '#type' => 'select', '#title' => t('Default country'), '#options' => array('site_default' => t('- Site default -')) + _addressfield_country_options_list(), '#default_value' => $settings['default_country'], '#empty_value' => '', ); $form['format_handlers'] = array( '#type' => 'checkboxes', '#title' => t('Format handlers'), '#options' => addressfield_format_plugins_options(), '#default_value' => $settings['format_handlers'], ); } return $form; } /** * Implements hook_form_BASE_FORM_ID_alter(). * * Removes the default values form from the field settings page. * Allows the module to implement its own, more predictable default value * handling, getting around #1253820 and other bugs. */ function addressfield_form_field_ui_field_edit_form_alter(&$form, $form_state) { if ($form['#field']['type'] == 'addressfield') { $form['instance']['default_value_widget']['#access'] = FALSE; } } /** * Implements hook_field_widget_form() */ function addressfield_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { $settings = $instance['widget']['settings']; $address = array(); // If the form has been rebuilt via AJAX, use the form state values. // $form_state['values'] is empty because of #limit_validation_errors, so // $form_state['input'] needs to be used instead. $parents = array_merge($element['#field_parents'], array($element['#field_name'], $langcode, $delta)); if (!empty($form_state['input'])) { $input_address = drupal_array_get_nested_value($form_state['input'], $parents); } if (!empty($input_address)) { $address = $input_address; } elseif (!empty($items[$delta]['country'])) { // Else use the saved value for the field. $address = $items[$delta]; } // Determine the list of available countries, and if the currently selected // country is not in it, unset it so it can be reset to the default country. $countries = _addressfield_country_options_list($field, $instance); if (!empty($address['country']) && !isset($countries[$address['country']])) { unset($address['country']); } // Merge in default values. $address += addressfield_default_values($field, $instance, $address); // Add the form elements for the standard widget, which includes a country // select list at the top that reloads the available address elements when the // country is changed. if ($instance['widget']['type'] == 'addressfield_standard') { // Wrap everything in a fieldset. This is not the best looking element, // but it's the only wrapper available in Drupal we can properly use // in that context, and it is overridable if necessary. $element['#type'] = 'fieldset'; if (!empty($instance['description'])) { // Checkout panes convert the fieldset into a container, causing // #description to not be rendered. Hence, a real element is added and // the old #description is removed. $element['#description'] = ''; $element['element_description'] = array( '#markup' => $instance['description'], '#prefix' => '
', '#suffix' => '
', '#weight' => -999, ); } // Generate the address form. $context = array( 'mode' => 'form', 'field' => $field, 'instance' => $instance, 'langcode' => $langcode, 'delta' => $delta, ); $element += addressfield_generate($address, $settings['format_handlers'], $context); // Remove any already saved default value. // See addressfield_form_field_ui_field_edit_form_alter() for the reasoning. if ($form_state['build_info']['form_id'] == 'field_ui_field_edit_form') { $element['#address'] = array('country' => ''); } } return $element; } /** * Element validate callback: rebuilds the form on country change. */ function addressfield_standard_country_validate($element, &$form_state) { if ($element['#default_value'] != $element['#value']) { $parents = $element['#parents']; array_pop($parents); $address = drupal_array_get_nested_value($form_state['values'], $parents); // Clear the country-specific field values. $country_specific_data = array( 'dependent_locality' => '', 'locality' => '', 'administrative_area' => '', 'postal_code' => '', ); $address = array_diff_key($address, $country_specific_data); drupal_array_set_nested_value($form_state['values'], $parents, $address); drupal_array_set_nested_value($form_state['input'], $parents, $address); $form_state['rebuild'] = TRUE; } } /** * Ajax callback in response to a change of country in an address field. * * The only thing we have to do is to find the proper element to render. */ function addressfield_standard_widget_refresh($form, $form_state) { // The target element is one element below the triggering country selector. $array_parents = $form_state['triggering_element']['#array_parents']; array_pop($array_parents); // Iterate over the form parents to find the element. $element = $form; foreach ($array_parents as $name) { $element = &$element[$name]; if (!empty($element['#addressfield'])) { break; } } // Return the address block, but remove the '_weight' element inserted // by the field API. unset($element['_weight']); // Replace the address field widget with the updated widget and focus on the // new country select list. $commands[] = ajax_command_replace(NULL, render($element)); $commands[] = ajax_command_invoke('#' . $element['country']['#id'], 'focus'); // Add the status messages inside the new addressfield's wrapper element, // just like core does. $commands[] = ajax_command_prepend(NULL, theme('status_messages')); // Allow other modules to add arbitrary AJAX commands on the refresh. drupal_alter('addressfield_standard_widget_refresh', $commands, $form, $form_state); return array('#type' => 'ajax', '#commands' => $commands); } /** * Implements hook_field_formatter_info(). */ function addressfield_field_formatter_info() { return array( 'addressfield_default' => array( 'label' => t('Default'), 'field types' => array('addressfield'), 'settings' => array( 'use_widget_handlers' => 1, 'format_handlers' => array('address'), ), ), ); } /** * Implements hook_field_formatter_settings_form(). */ function addressfield_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) { $display = $instance['display'][$view_mode]; $settings = $display['settings']; $element['use_widget_handlers'] = array( '#type' => 'checkbox', '#title' => t('Use the same configuration as the widget.'), '#default_value' => !empty($settings['use_widget_handlers']), ); $element['format_handlers'] = array( '#type' => 'checkboxes', '#title' => t('Format handlers'), '#options' => addressfield_format_plugins_options(), '#default_value' => $settings['format_handlers'], '#process' => array('form_process_checkboxes', '_addressfield_field_formatter_settings_form_process_add_state'), '#element_validate' => array('_addressfield_field_formatter_settings_form_validate') ); return $element; } /** * Helper function: set the proper #states to the use widget handlers checkbox. */ function _addressfield_field_formatter_settings_form_process_add_state($element, $form_state) { // Build a #parents based on the current checkbox. $target_parents = array_slice($element['#parents'], 0, -1); $target_parents[] = 'use_widget_handlers'; $target_parents = array_shift($target_parents) . ($target_parents ? '[' . implode('][', $target_parents) . ']' : ''); $element['#states']['visible'] = array( ':input[name="' . $target_parents . '"]' => array('checked' => FALSE), ); return $element; } /** * Helper function: filter the results of the checkboxes form element. */ function _addressfield_field_formatter_settings_form_validate($element, &$element_state) { form_set_value($element, array_filter($element['#value']), $element_state); } /** * Implements hook_field_formatter_settings_summary(). */ function addressfield_field_formatter_settings_summary($field, $instance, $view_mode) { $display = $instance['display'][$view_mode]; $settings = $display['settings']; if ($settings['use_widget_handlers']) { return t('Use widget configuration'); } else { $summary = array(); $plugins = addressfield_format_plugins(); foreach ($settings['format_handlers'] as $handler) { $summary[] = $plugins[$handler]['title']; } return $summary ? implode(', ', $summary) : t('No handler'); } } /** * Implements hook_field_formatter_view(). */ function addressfield_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { $settings = $display['settings']; $element = array(); switch ($display['type']) { case 'addressfield_default': if (!empty($settings['use_widget_handlers'])) { $handlers = $instance['widget']['settings']['format_handlers']; } else { $handlers = $settings['format_handlers']; } foreach ($items as $delta => $address) { // Generate the address format. $context = array( 'mode' => 'render', 'field' => $field, 'instance' => $instance, 'langcode' => $langcode, 'delta' => $delta, ); $element[$delta] = addressfield_generate($address, $handlers, $context); } break; } return $element; } /** * Callback to alter the property info of address fields. * * @see addressfield_field_info(). */ function addressfield_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) { $name = $field['field_name']; $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$name]; $property['type'] = ($field['cardinality'] != 1) ? 'list' : 'addressfield'; $property['getter callback'] = 'entity_metadata_field_verbatim_get'; $property['setter callback'] = 'entity_metadata_field_verbatim_set'; $property['auto creation'] = 'addressfield_auto_creation'; $property['property info'] = addressfield_data_property_info(); unset($property['query callback']); } /** * Auto creation callback for an addressfield value array. * * @see addressfield_property_info_callback() */ function addressfield_auto_creation($property_name, $context) { return addressfield_default_values($context['field'], $context['instance']); } /** * Defines info for the properties of the address field data structure. */ function addressfield_data_property_info($name = NULL) { // Build an array of basic property information for the address field. $properties = array( 'country' => array( 'label' => t('Country'), 'options list' => '_addressfield_country_options_list', ), 'name_line' => array( 'label' => t('Full name'), ), 'first_name' => array( 'label' => t('First name'), ), 'last_name' => array( 'label' => t('Last name'), ), 'organisation_name' => array( 'label' => t('Company'), ), 'administrative_area' => array( 'label' => t('Administrative area (i.e. State / Province)'), ), 'sub_administrative_area' => array( 'label' => t('Sub administrative area'), ), 'locality' => array( 'label' => t('Locality (i.e. City)'), ), 'dependent_locality' => array( 'label' => t('Dependent locality'), ), 'postal_code' => array( 'label' => t('Postal code'), ), 'thoroughfare' => array( 'label' => t('Thoroughfare (i.e. Street address)'), ), 'premise' => array( 'label' => t('Premise (i.e. Apartment / Suite number)'), ), 'sub_premise' => array( 'label' => t('Sub Premise (i.e. Suite, Apartment, Floor, Unknown.'), ), ); // Add the default values for each of the address field properties. foreach ($properties as $key => &$value) { $value += array( 'description' => !empty($name) ? t('!label of field %name', array('!label' => $value['label'], '%name' => $name)) : '', 'type' => 'text', 'getter callback' => 'entity_property_verbatim_get', 'setter callback' => 'entity_property_verbatim_set', ); } return $properties; } /** * Returns the country list in a format suitable for use as an options list. */ function _addressfield_country_options_list($field = NULL, $instance = NULL) { if (module_exists('countries')) { $countries = countries_get_countries('name', array('enabled' => COUNTRIES_ENABLED)); } else { require_once DRUPAL_ROOT . '/includes/locale.inc'; $countries = country_get_list(); } if (isset($field)) { // If the instance is not specified, loop against all the instances of the field. if (!isset($instance)) { $instances = array(); foreach ($field['bundles'] as $entity_type => $bundles) { foreach ($bundles as $bundle_name) { $instances[] = field_info_instance($entity_type, $field['field_name'], $bundle_name); } } } else { $instances = array($instance); } foreach ($instances as $instance) { if (!empty($instance['widget']['settings']['available_countries'])) { $countries = array_intersect_key($countries, $instance['widget']['settings']['available_countries']); break; } } } return $countries; }