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 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 .= '' . $element['#tag'] . ">"; 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(str_replace(' ', ' ', $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' => '