Commit c6448794 authored by bojanz's avatar bojanz

Issue #2727777 by DuneBL, bojanz: Add a country field type

parent 21d629b5
......@@ -15,34 +15,45 @@ use Drupal\field\FieldStorageConfigInterface;
* Views integration for address fields.
*/
function address_field_views_data(FieldStorageConfigInterface $field) {
// Provide a field handler for each individual column.
$columns = [
'country_code' => 'country_code',
'administrative_area' => 'subdivision',
'locality' => 'subdivision',
'dependent_locality' => 'subdivision',
'postal_code' => 'standard',
'sorting_code' => 'standard',
'address_line1' => 'standard',
'address_line2' => 'standard',
'organization' => 'standard',
'given_name' => 'standard',
'additional_name' => 'standard',
'family_name' => 'standard',
];
$data = views_field_default_views_data($field);
$field_type = $field->getType();
$field_name = $field->getName();
foreach ($data as $table_name => $table_data) {
foreach ($columns as $column => $plugin_id) {
$data[$table_name][$field_name . '_' . $column]['field'] = [
'id' => $plugin_id,
if ($field_type == 'address') {
$columns = [
'country_code' => 'country_code',
'administrative_area' => 'subdivision',
'locality' => 'subdivision',
'dependent_locality' => 'subdivision',
'postal_code' => 'standard',
'sorting_code' => 'standard',
'address_line1' => 'standard',
'address_line2' => 'standard',
'organization' => 'standard',
'given_name' => 'standard',
'additional_name' => 'standard',
'family_name' => 'standard',
];
foreach ($data as $table_name => $table_data) {
foreach ($columns as $column => $plugin_id) {
$data[$table_name][$field_name . '_' . $column]['field'] = [
'id' => $plugin_id,
'field_name' => $field_name,
'property' => $column,
];
}
// Add the custom country_code filter.
$data[$table_name][$field_name . '_country_code']['filter']['id'] = 'country_code';
}
}
elseif ($field_type == 'address_country') {
foreach ($data as $table_name => $table_data) {
$data[$table_name][$field_name . '_value']['field'] = [
'id' => 'country_code',
'field_name' => $field_name,
'property' => $column,
'property' => 'value',
];
$data[$table_name][$field_name . '_value']['filter']['id'] = 'country_code';
}
// Add the custom country_code filter.
$data[$table_name][$field_name . '_country_code']['filter']['id'] = 'country_code';
}
return $data;
......
......@@ -91,6 +91,24 @@ field.widget.settings.address_default:
type: string
label: 'Default country'
field.value.address_country:
type: mapping
label: 'Default value'
mapping:
value:
type: string
label: 'Country code'
field.field_settings.address_country:
type: mapping
label: 'Country field settings'
mapping:
available_countries:
type: sequence
label: 'Available countries'
sequence:
- type: string
views.filter.country_code:
type: views.filter.in_operator
label: 'Country'
......@@ -78,27 +78,28 @@ class Address extends FormElement {
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if (is_array($input)) {
return $input;
$value = $input;
}
else {
if (!is_array($element['#default_value'])) {
$element['#default_value'] = [];
}
// Initialize properties.
$properties = [
'given_name', 'additional_name', 'family_name', 'organization',
'address_line1', 'address_line2', 'postal_code', 'sorting_code',
'dependent_locality', 'locality', 'administrative_area',
'country_code', 'langcode',
];
foreach ($properties as $property) {
if (!isset($element['#default_value'][$property])) {
$element['#default_value'][$property] = NULL;
}
$value = $element['#default_value'];
}
// Initialize default keys.
$properties = [
'given_name', 'additional_name', 'family_name', 'organization',
'address_line1', 'address_line2', 'postal_code', 'sorting_code',
'dependent_locality', 'locality', 'administrative_area',
'country_code', 'langcode',
];
foreach ($properties as $property) {
if (!isset($value[$property])) {
$value[$property] = NULL;
}
return $element['#default_value'];
}
return $value;
}
/**
......@@ -115,36 +116,16 @@ class Address extends FormElement {
* The processed element.
*
* @throws \InvalidArgumentException
* Thrown when #available_countries or #used_fields is malformed.
* Thrown when #used_fields is malformed.
*/
public static function processAddress(array &$element, FormStateInterface $form_state, array &$complete_form) {
if (isset($element['#available_countries']) && !is_array($element['#available_countries'])) {
throw new \InvalidArgumentException('The #available_countries property must be an array.');
}
if (isset($element['#used_fields']) && !is_array($element['#used_fields'])) {
throw new \InvalidArgumentException('The #used_fields property must be an array.');
}
$id_prefix = implode('-', $element['#parents']);
$wrapper_id = Html::getUniqueId($id_prefix . '-ajax-wrapper');
$full_country_list = \Drupal::service('address.country_repository')->getList();
$country_list = $full_country_list;
if (!empty($element['#available_countries'])) {
$available_countries = $element['#available_countries'];
if (!empty($element['#default_value']['country_code'])) {
// The current country should always be available.
$available_countries[] = $element['#default_value']['country_code'];
}
$available_countries = array_combine($available_countries, $available_countries);
$country_list = array_intersect_key($country_list, $available_countries);
}
$value = $element['#value'];
if (empty($value['country_code']) && $element['#required']) {
// Fallback to the first country in the list if the default country
// is empty even though the field is required.
$value['country_code'] = key($country_list);
}
$element = [
'#tree' => TRUE,
......@@ -157,35 +138,19 @@ class Address extends FormElement {
'#type' => 'hidden',
'#value' => $value['langcode'],
];
// Hide the country dropdown when there is only one possible value.
if (count($country_list) == 1 && $element['#required']) {
$element['country_code'] = [
'#type' => 'hidden',
'#value' => key($available_countries),
];
}
else {
$element['country_code'] = [
'#type' => 'select',
'#title' => t('Country'),
'#options' => $country_list,
'#default_value' => $value['country_code'],
'#required' => $element['#required'],
'#limit_validation_errors' => [],
'#ajax' => [
'callback' => [get_called_class(), 'ajaxRefresh'],
'wrapper' => $wrapper_id,
],
'#attributes' => [
'class' => ['country'],
'autocomplete' => 'country',
],
'#weight' => -100,
];
if (!$element['#required']) {
$element['country_code']['#empty_value'] = '';
}
}
$element['country_code'] = [
'#type' => 'address_country',
'#title' => t('Country'),
'#available_countries' => $element['#available_countries'],
'#default_value' => $value['country_code'],
'#required' => $element['#required'],
'#limit_validation_errors' => [],
'#ajax' => [
'callback' => [get_called_class(), 'ajaxRefresh'],
'wrapper' => $wrapper_id,
],
'#weight' => -100,
];
if (!empty($value['country_code'])) {
$element = static::addressElements($element, $value);
}
......@@ -366,7 +331,7 @@ class Address extends FormElement {
*/
public static function ajaxRefresh(array $form, FormStateInterface $form_state) {
$country_element = $form_state->getTriggeringElement();
$address_element = NestedArray::getValue($form, array_slice($country_element['#array_parents'], 0, -1));
$address_element = NestedArray::getValue($form, array_slice($country_element['#array_parents'], 0, -2));
return $address_element;
}
......
<?php
namespace Drupal\address\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\FormElement;
/**
* Provides a country form element.
*
* Usage example:
* @code
* $form['country'] = [
* '#type' => 'address_country',
* '#default_value' => 'DE',
* '#available_countries' => ['DE', 'FR'],
* ];
* @endcode
*
* @FormElement("address_country")
*/
class Country extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
// List of country codes. If empty, all countries will be available.
'#available_countries' => [],
'#input' => TRUE,
'#multiple' => FALSE,
'#default_value' => NULL,
'#process' => [
[$class, 'processCountry'],
[$class, 'processGroup'],
],
'#theme_wrappers' => ['container'],
];
}
/**
* Processes the address_country form element.
*
* @param array $element
* The form element to process.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed element.
*
* @throws \InvalidArgumentException
* Thrown when #available_countries is malformed.
*/
public static function processCountry(array &$element, FormStateInterface $form_state, array &$complete_form) {
if (isset($element['#available_countries']) && !is_array($element['#available_countries'])) {
throw new \InvalidArgumentException('The #available_countries property must be an array.');
}
$full_country_list = \Drupal::service('address.country_repository')->getList();
$country_list = $full_country_list;
if (!empty($element['#available_countries'])) {
$available_countries = $element['#available_countries'];
if (!empty($element['#default_value'])) {
// The current country should always be available.
$available_countries[] = $element['#default_value'];
}
$available_countries = array_combine($available_countries, $available_countries);
$country_list = array_intersect_key($country_list, $available_countries);
}
$value = $element['#value'];
if (empty($value) && $element['#required']) {
// Fallback to the first country in the list if the default country
// is empty even though the field is required.
$value = key($country_list);
}
$element['#tree'] = TRUE;
// Hide the dropdown when there is only one possible value.
if (count($country_list) == 1 && $element['#required']) {
$element['country_code'] = [
'#type' => 'hidden',
'#value' => key($available_countries),
];
}
else {
$element['country_code'] = [
'#type' => 'select',
'#title' => $element['#title'],
'#options' => $country_list,
'#default_value' => $value,
'#required' => $element['#required'],
'#limit_validation_errors' => [],
'#attributes' => [
'class' => ['country'],
'autocomplete' => 'country',
],
'#weight' => -100,
];
if (!$element['#required']) {
$element['country_code']['#empty_value'] = '';
}
if (!empty($element['#ajax'])) {
$element['country_code']['#ajax'] = $element['#ajax'];
unset($element['#ajax']);
}
}
// Remove the 'country_code' level from form state values.
$element['country_code']['#parents'] = $element['#parents'];
return $element;
}
}
<?php
namespace Drupal\address\Plugin\Field\FieldFormatter;
use CommerceGuys\Addressing\Country\CountryRepositoryInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the 'country_default' formatter.
*
* @FieldFormatter(
* id = "address_country_default",
* label = @Translation("Default"),
* field_types = {
* "address_country",
* },
* )
*/
class CountryDefaultFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
/**
* The country repository.
*
* @var \CommerceGuys\Addressing\Country\CountryRepositoryInterface
*/
protected $countryRepository;
/**
* Constructs an CountryDefaultFormatter object.
*
* @param string $plugin_id
* The plugin_id for the formatter.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the formatter is associated.
* @param array $settings
* The formatter settings.
* @param string $label
* The formatter label display setting.
* @param string $view_mode
* The view mode.
* @param array $third_party_settings
* Any third party settings.
* @param \CommerceGuys\Addressing\Country\CountryRepositoryInterface $country_repository
* The country repository.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, CountryRepositoryInterface $country_repository) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->countryRepository = $country_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['label'],
$configuration['view_mode'],
$configuration['third_party_settings'],
$container->get('address.country_repository')
);
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$countries = $this->countryRepository->getList();
$elements = [];
foreach ($items as $delta => $item) {
$elements[$delta] = [
'#plain_text' => isset($countries[$item->value]) ? $countries[$item->value] : $item->value,
'#cache' => [
'contexts' => [
'languages:' . LanguageInterface::TYPE_INTERFACE,
],
],
];
}
return $elements;
}
}
......@@ -3,8 +3,6 @@
namespace Drupal\address\Plugin\Field\FieldType;
use CommerceGuys\Addressing\AddressFormat\AddressField;
use Drupal\address\Event\AddressEvents;
use Drupal\address\Event\AvailableCountriesEvent;
use Drupal\address\AddressInterface;
use Drupal\address\LabelHelper;
use Drupal\Core\Field\FieldItemBase;
......@@ -20,18 +18,14 @@ use Drupal\Core\TypedData\DataDefinition;
* id = "address",
* label = @Translation("Address"),
* description = @Translation("An entity field containing a postal address"),
* category = @Translation("Address"),
* default_widget = "address_default",
* default_formatter = "address_default"
* )
*/
class AddressItem extends FieldItemBase implements AddressInterface {
/**
* An altered list of available countries.
*
* @var array
*/
protected static $availableCountries = [];
use AvailableCountriesTrait;
/**
* {@inheritdoc}
......@@ -141,11 +135,10 @@ class AddressItem extends FieldItemBase implements AddressInterface {
* {@inheritdoc}
*/
public static function defaultFieldSettings() {
return [
'available_countries' => [],
return self::defaultCountrySettings() + [
'fields' => array_values(AddressField::getAll()),
'langcode_override' => '',
] + parent::defaultFieldSettings();
];
}
/**
......@@ -161,16 +154,7 @@ class AddressItem extends FieldItemBase implements AddressInterface {
}
}
$element = [];
$element['available_countries'] = [
'#type' => 'select',
'#title' => $this->t('Available countries'),
'#description' => $this->t('If no countries are selected, all countries will be available.'),
'#options' => \Drupal::service('address.country_repository')->getList(),
'#default_value' => $this->getSetting('available_countries'),
'#multiple' => TRUE,
'#size' => 10,
];
$element = $this->countrySettingsForm($form, $form_state);
$element['fields'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Used fields'),
......@@ -192,27 +176,6 @@ class AddressItem extends FieldItemBase implements AddressInterface {
return $element;
}
/**
* Gets the available countries for the current field.
*
* @return array
* A list of country codes.
*/
public function getAvailableCountries() {
// Alter the list once per field, instead of once per field delta.
$field_definition = $this->getFieldDefinition();
$definition_id = spl_object_hash($field_definition);
if (!isset(static::$availableCountries[$definition_id])) {
$available_countries = array_filter($this->getSetting('available_countries'));
$event_dispatcher = \Drupal::service('event_dispatcher');
$event = new AvailableCountriesEvent($available_countries, $field_definition);
$event_dispatcher->dispatch(AddressEvents::AVAILABLE_COUNTRIES, $event);
static::$availableCountries[$definition_id] = $event->getAvailableCountries();
}
return static::$availableCountries[$definition_id];
}
/**
* Initializes and returns the langcode property for the current field.
*
......@@ -262,11 +225,16 @@ class AddressItem extends FieldItemBase implements AddressInterface {
*/
public function getConstraints() {
$constraints = parent::getConstraints();
$manager = \Drupal::typedDataManager()->getValidationConstraintManager();
$available_countries = $this->getAvailableCountries();
$constraint_manager = \Drupal::typedDataManager()->getValidationConstraintManager();
$enabled_fields = array_filter($this->getSetting('fields'));
$constraints[] = $manager->create('Country', ['availableCountries' => $available_countries]);
$constraints[] = $manager->create('AddressFormat', ['fields' => $enabled_fields]);
$constraints[] = $constraint_manager->create('ComplexData', [
'country_code' => [
'Country' => [
'availableCountries' => $this->getAvailableCountries(),
],
],
]);
$constraints[] = $constraint_manager->create('AddressFormat', ['fields' => $enabled_fields]);
return $constraints;
}
......
<?php
namespace Drupal\address\Plugin\Field\FieldType;
use Drupal\address\Event\AddressEvents;
use Drupal\address\Event\AvailableCountriesEvent;
use Drupal\Core\Form\FormStateInterface;
/**
* Allows field types to limit the available countries.
*/
trait AvailableCountriesTrait {
/**
* An altered list of available countries.
*
* @var array
*/
protected static $availableCountries = [];
/**
* Defines the default field-level settings.
*
* @return array
* A list of default settings, keyed by the setting name.
*/
public static function defaultCountrySettings() {
return [
'available_countries' => [],
];
}
/**
* Builds the field settings form.
*
* @param array $form
* The form where the settings form is being included in.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the (entire) configuration form.
*
* @return array
* The modified form.
*/
public function countrySettingsForm(array $form, FormStateInterface $form_state) {
$element = [];
$element['available_countries'] = [
'#type' => 'select',
'#title' => $this->t('Available countries'),
'#description' => $this->t('If no countries are selected, all countries will be available.'),
'#options' => \Drupal::service('address.country_repository')->getList(),
'#default_value' => $this->getSetting('available_countries'),
'#multiple' => TRUE,
'#size' => 10,