field_group.module in Field Group 8
Same filename and directory in other branches
Allows administrators to attach custom fields to fieldable types.
File
field_group.moduleView source
<?php
/**
* @file
* Allows administrators to attach custom fields to fieldable types.
*/
use Drupal\Core\Entity\ContentEntityFormInterface;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\ConfirmFormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
require_once __DIR__ . '/includes/helpers.inc';
/**
* Implements hook_theme_registry_alter().
*/
function field_group_theme_registry_alter(&$theme_registry) {
// Inject field_group_build_entity_groups in all entity theming functions.
$entity_info = Drupal::entityTypeManager()
->getDefinitions();
$entity_types = array();
foreach ($entity_info as $entity_type_id => $entity_type) {
if ($route_name = $entity_type
->get('field_ui_base_route')) {
$entity_types[] = $entity_type_id;
}
}
foreach ($theme_registry as $theme_hook => $info) {
if (in_array($theme_hook, $entity_types) || !empty($info['base hook']) && in_array($info['base hook'], $entity_types)) {
$theme_registry[$theme_hook]['preprocess functions'][] = 'field_group_build_entity_groups';
}
}
// ECK does not use the eck as theme function.
if (isset($theme_registry['eck_entity'])) {
$theme_registry['eck_entity']['preprocess functions'][] = 'field_group_build_entity_groups';
}
}
/**
* Implements hook_theme().
*/
function field_group_theme() {
return array(
'horizontal_tabs' => array(
'render element' => 'element',
'template' => 'horizontal-tabs',
'file' => 'templates/theme.inc',
),
'field_group_accordion_item' => array(
'render element' => 'element',
'template' => 'field-group-accordion-item',
'file' => 'templates/theme.inc',
),
'field_group_accordion' => array(
'render element' => 'element',
'template' => 'field-group-accordion',
'file' => 'templates/theme.inc',
),
'field_group_html_element' => array(
'render element' => 'element',
'template' => 'field-group-html-element',
'file' => 'templates/theme.inc',
),
);
}
/**
* Implements hook_theme_suggestions_alter().
*
* @param array $suggestions
* @param array $variables
* @param $hook
*/
function field_group_theme_suggestions_alter(array &$suggestions, array $variables, $hook) {
switch ($hook) {
case 'horizontal_tabs':
case 'field_group_accordion_item':
case 'field_group_accordion':
case 'field_group_html_element':
$element = $variables['element'];
$name = $element['#group_name'];
$entity_type = $element['#entity_type'];
$bundle = $element['#bundle'];
$wrapper = '';
if (isset($element['#wrapper_element'])) {
$wrapper = $element['#wrapper_element'];
$suggestions[] = $hook . '__' . $wrapper;
}
$suggestions[] = $hook . '__' . $entity_type;
$suggestions[] = $hook . '__' . $bundle;
$suggestions[] = $hook . '__' . $name;
if ($wrapper) {
$suggestions[] = $hook . '__' . $entity_type . '__' . $wrapper;
}
$suggestions[] = $hook . '__' . $entity_type . '__' . $bundle;
$suggestions[] = $hook . '__' . $entity_type . '__' . $name;
if ($wrapper) {
$suggestions[] = $hook . '__' . $entity_type . '__' . $bundle . '__' . $wrapper;
}
$suggestions[] = $hook . '__' . $entity_type . '__' . $bundle . '__' . $name;
break;
}
}
/**
* Implements hook_form_FORM_ID_alter().
* Using hook_form_field_ui_form_display_overview_form_alter.
*/
function field_group_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
$form_state
->loadInclude('field_group', 'inc', 'includes/field_ui');
field_group_field_ui_display_form_alter($form, $form_state);
}
/**
* Implements hook_form_FORM_ID_alter().
* Using hook_form_field_ui_display_overview_form_alter.
*/
function field_group_form_entity_view_display_edit_form_alter(&$form, FormStateInterface $form_state) {
$form_state
->loadInclude('field_group', 'inc', 'includes/field_ui');
field_group_field_ui_display_form_alter($form, $form_state);
}
/**
* Implements hook_field_info_max_weight().
*/
function field_group_field_info_max_weight($entity_type, $bundle, $context, $context_mode) {
// Prevent recursion.
// @see https://www.drupal.org/project/drupal/issues/2966137
static $recursion_tracker = [];
// Track the entity type.
$key = $entity_type . ':' . $bundle . ':' . $context . ':' . $context_mode;
// If entity type check was attempted but did not finish, do not continue.
if (isset($recursion_tracker[$key])) {
return NULL;
}
// Mark this as an attempt at entity type check.
$recursion_tracker[$key] = TRUE;
$groups = field_group_info_groups($entity_type, $bundle, $context, $context_mode);
// Remove the indicator once the entity is successfully checked.
unset($recursion_tracker[$key]);
$weights = array();
foreach ($groups as $group) {
$weights[] = $group->weight;
}
return $weights ? max($weights) : NULL;
}
/**
* Implements hook_form_alter().
*/
function field_group_form_alter(array &$form, FormStateInterface $form_state) {
$form_object = $form_state
->getFormObject();
if ($form_object instanceof ContentEntityFormInterface && !$form_object instanceof ConfirmFormInterface) {
/**
* @var EntityFormDisplayInterface $form_display
*/
$storage = $form_state
->getStorage();
if (!empty($storage['form_display'])) {
$form_display = $storage['form_display'];
$entity = $form_object
->getEntity();
$context = array(
'entity_type' => $entity
->getEntityTypeId(),
'bundle' => $entity
->bundle(),
'entity' => $entity,
'context' => 'form',
'display_context' => 'form',
'mode' => $form_display
->getMode(),
);
field_group_attach_groups($form, $context);
$form['#pre_render'][] = 'field_group_form_pre_render';
}
}
}
/**
* Implements hook_inline_entity_form_entity_form_alter().
*/
function field_group_inline_entity_form_entity_form_alter(&$entity_form, FormStateInterface $form_state) {
// Attach the fieldgroups to current entity form.
$context = [
'entity_type' => $entity_form['#entity']
->getEntityTypeId(),
'bundle' => $entity_form['#entity']
->bundle(),
'entity' => $entity_form['#entity'],
'display_context' => 'form',
'mode' => isset($entity_form['#form_mode']) ? $entity_form['#form_mode'] : 'default',
];
field_group_attach_groups($entity_form, $context);
$entity_form['#pre_render'][] = 'field_group_form_pre_render';
}
/**
* Implements hook_entity_view_alter().
*/
function field_group_entity_view_alter(&$build, EntityInterface $entity, EntityDisplayInterface $display) {
$context = array(
'entity_type' => $display
->getTargetEntityTypeId(),
'bundle' => $entity
->bundle(),
'entity' => $entity,
'display_context' => 'view',
'mode' => $display
->getMode(),
);
field_group_attach_groups($build, $context);
// If no theme hook, we have no theme hook to preprocess.
// Add a prerender.
if (empty($build['#theme'])) {
$ds_enabled = false;
if (Drupal::moduleHandler()
->moduleExists('ds')) {
// Check if DS is enabled for this display.
if ($display
->getThirdPartySetting('ds', 'layout') && !Drupal\ds\Ds::isDisabled()) {
$ds_enabled = true;
}
}
// If DS is enabled, no pre render is needed (DS adds fieldgroup preprocessing).
if (!$ds_enabled) {
$build['#pre_render'][] = 'field_group_entity_view_pre_render';
}
}
}
/**
* Pre render callback for rendering groups.
* @see field_group_field_attach_form
* @param $element Form that is being rendered.
*/
function field_group_form_pre_render($element) {
if (empty($element['#field_group_form_pre_render'])) {
$element['#field_group_form_pre_render'] = TRUE;
field_group_build_entity_groups($element, 'form');
}
return $element;
}
/**
* Pre render callback for rendering groups on entities without theme hook.
* @param $element
* Entity being rendered.
*/
function field_group_entity_view_pre_render($element) {
field_group_build_entity_groups($element, 'view');
return $element;
}
/**
* Implements hook_field_group_pre_render().
*
* @param Array $element
* Group beïng rendered.
* @param Object $group
* The Field group info.
* @param $rendering_object
* The entity / form beïng rendered
*/
function field_group_field_group_pre_render(&$element, &$group, &$rendering_object) {
// Add all field_group format types to the js settings.
$element['#attached']['drupalSettings']['field_group'] = array(
$group->format_type => [
'mode' => $group->mode,
'context' => $group->context,
'settings' => $group->format_settings,
],
);
$element['#weight'] = $group->weight;
// Call the pre render function for the format type.
$manager = Drupal::service('plugin.manager.field_group.formatters');
$plugin = $manager
->getInstance(array(
'format_type' => $group->format_type,
'configuration' => array(
'label' => $group->label,
'settings' => $group->format_settings,
),
'group' => $group,
));
$plugin
->preRender($element, $rendering_object);
}
/**
* Implements hook_field_group_build_pre_render_alter().
* @param Array $elements by address.
*/
function field_group_field_group_build_pre_render_alter(&$element) {
// Someone is doing a node view, in a node view. Reset content.
if (isset($element['#node']->content) && count($element['#node']->content) > 0) {
$element['#node']->content = array();
}
$display = isset($element['#view_mode']);
$groups = array_keys($element['#fieldgroups']);
// Dish the fieldgroups with no fields for non-forms.
if ($display) {
field_group_remove_empty_display_groups($element, $groups);
}
else {
// Fix the problem on forms with additional settings.
field_group_remove_empty_form_groups('form', $element, $groups, $element['#fieldgroups'], $element['#entity_type']);
}
}
/**
* Attach groups to the (form) build.
*
* @param Array $element
* The part of the form.
* @param Array $context
* The contextual information.
*/
function field_group_attach_groups(&$element, $context) {
$entity_type = $context['entity_type'];
$bundle = $context['bundle'];
$mode = $context['mode'];
$display_context = $context['display_context'];
$element['#fieldgroups'] = field_group_info_groups($entity_type, $bundle, $display_context, $mode);
// Create a lookup array.
$group_children = array();
foreach ($element['#fieldgroups'] as $group_name => $group) {
foreach ($group->children as $child) {
$group_children[$child] = $group_name;
}
}
$element['#group_children'] = $group_children;
$element['#entity_type'] = $entity_type;
}
/**
* Preprocess/ Pre-render callback.
*
* @see field_group_form_pre_render()
* @see field_group_theme_registry_alter
* @see field_group_fields_nest()
* @param $vars preprocess vars or form element
* @param $context The display context (entity type, form or view)
* @return $element Array with re-arranged fields in groups.
*/
function field_group_build_entity_groups(&$vars, $context = 'view') {
if ($context == 'form') {
$element =& $vars;
$nest_vars = NULL;
}
else {
if (isset($vars['elements'])) {
$element =& $vars['elements'];
}
elseif (isset($vars['content'])) {
$element =& $vars['content'];
}
else {
if ($context === 'eck_entity') {
$element =& $vars['entity'];
}
else {
$element =& $vars;
}
}
$nest_vars =& $vars;
}
// No groups on the entity.
if (empty($element['#fieldgroups'])) {
return $element;
}
// Nest the fields in the corresponding field groups.
field_group_fields_nest($element, $nest_vars, $context);
// Allow others to alter the pre_rendered build.
Drupal::moduleHandler()
->alter('field_group_build_pre_render', $element);
// Return the element on forms.
if ($context == 'form') {
return $element;
}
// No groups on the entity. Prerender removed empty field groups.
if (empty($element['#fieldgroups'])) {
return $element;
}
// Put groups inside content if we are rendering an entity_view.
foreach ($element['#fieldgroups'] as $group) {
if (!empty($element[$group->group_name])) {
$key = field_group_get_content_element_key($context);
if (isset($vars[$key])) {
$vars[$key][$group->group_name] = $element[$group->group_name];
}
}
}
}
/**
* Recursive function to nest fields in the field groups.
*
* This function will take out all the elements in the form and
* place them in the correct container element, a fieldgroup.
* The current group element in the loop is passed recursively so we can
* stash fields and groups in it while we go deeper in the array.
* @param Array $element
* The current element to analyse for grouping.
* @param Array $vars
* Rendering vars from the entity being viewed.
* @param Array $context
* The display context (entity type, form or view).
*/
function field_group_fields_nest(&$element, &$vars = NULL, $context = NULL) {
// Create all groups and keep a flat list of references to these groups.
$group_references = array();
foreach ($element['#fieldgroups'] as $group_name => $group) {
// Construct own weight, as some fields (for example preprocess fields) don't have weight set.
$element[$group_name] = array();
$group_references[$group_name] =& $element[$group_name];
}
// Loop through all form children looking for those that are supposed to be
// in groups, and insert placeholder element for the new group field in the
// correct location within the form structure.
$element_clone = array();
foreach (Element::children($element) as $child_name) {
$element_clone[$child_name] = $element[$child_name];
// If this element is in a group, create the placeholder element.
if (isset($element['#group_children'][$child_name])) {
$element_clone[$element['#group_children'][$child_name]] = array();
}
}
$element = array_merge($element_clone, $element);
// Move all children to their parents. Use the flat list of references for
// direct access as we don't know where in the root_element hierarchy the
// parent currently is situated.
foreach ($element['#group_children'] as $child_name => $parent_name) {
// Entity being viewed
if ($vars) {
// If not a group, check the content variable for empty field.
$key = field_group_get_content_element_key($context);
if (!isset($element['#fieldgroups'][$child_name]) && isset($vars[$key][$child_name])) {
$group_references[$parent_name][$child_name] = $vars[$key][$child_name];
unset($vars[$key][$child_name]);
}
else {
$group_references[$parent_name][$child_name] =& $element[$child_name];
unset($element[$child_name]);
}
}
else {
// Block denied fields (#access) before they are put in groups.
// Fields (not groups) that don't have children (like field_permissions) are removed
// in field_group_field_group_build_pre_render_alter.
if (isset($element[$child_name]) && (!isset($element[$child_name]['#access']) || $element[$child_name]['#access'])) {
// If this is a group, we have to use a reference to keep the reference
// list intact (but if it is a field we don't mind).
$group_references[$parent_name][$child_name] =& $element[$child_name];
$group_references[$parent_name]['#weight'] = $element['#fieldgroups'][$parent_name]->weight;
}
// The child has been copied to its parent: remove it from the root element.
unset($element[$child_name]);
}
}
// Bring extra element wrappers to achieve a grouping of fields.
// This will mainly be prefix and suffix altering.
foreach ($element['#fieldgroups'] as $group_name => $group) {
field_group_pre_render($group_references[$group_name], $group, $element);
}
}
/**
* Function to pre render the field group element.
*
* @see field_group_fields_nest()
*
* @param $element
* Render array of group element that needs to be created.
* @param $group
* Object with the group information.
* @param $rendering_object
* The entity / form beïng rendered.
*/
function field_group_pre_render(&$element, $group, &$rendering_object) {
// Only run the pre_render function if the group has elements.
// $group->group_name
if ($element == array()) {
return;
}
// Let modules define their wrapping element.
// Note that the group element has no properties, only elements.
foreach (Drupal::moduleHandler()
->getImplementations('field_group_pre_render') as $module) {
// The intention here is to have the opportunity to alter the
// elements, as defined in hook_field_group_formatter_info.
// Note, implement $element by reference!
$function = $module . '_field_group_pre_render';
$function($element, $group, $rendering_object);
}
// Allow others to alter the pre_render.
Drupal::moduleHandler()
->alter('field_group_pre_render', $element, $group, $rendering_object);
}
/**
* Provides the content element key for a display context.
*
* This allows entity modules to specify their content element for field group
* support, or other modules to add entity module support.
*
* @param $context
* The display context (entity type, form or view).
*/
function field_group_get_content_element_key($context = 'default') {
$keys =& drupal_static('field_group_content_elements');
if (!isset($keys)) {
$keys['default'] = 'content';
// Allow other modules to alter the array.
Drupal::moduleHandler()
->alter('field_group_content_element_keys', $keys);
}
// Check if we have a specific content element key for this entity type.
$key = $keys['default'];
if (isset($keys[$context])) {
$key = $keys[$context];
}
return $key;
}
/**
* Saves a group definition.
*
* This function is called by ctools export when calls are made through
* ctools_export_crud_save(). It's also used as an api method to add groups to a
* display.
*
* @param \stdClass $group
* A group definition.
* @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display
* The display to update if known.
*
* @return \Drupal\Core\Entity\Display\EntityDisplayInterface|NULL
* The updated entity display.
*/
function field_group_group_save($group, $display = NULL) {
if ($display === NULL) {
if ($group->context == 'form') {
$display = EntityFormDisplay::load($group->entity_type . '.' . $group->bundle . '.' . $group->mode);
}
elseif ($group->context == 'view') {
$display = EntityViewDisplay::load($group->entity_type . '.' . $group->bundle . '.' . $group->mode);
}
}
// If no display was found. It doesn't exist yet, create it.
if (!isset($display)) {
if ($group->context == 'form') {
$display = EntityFormDisplay::create(array(
'targetEntityType' => $group->entity_type,
'bundle' => $group->bundle,
'mode' => $group->mode,
))
->setStatus(TRUE);
}
elseif ($group->context == 'view') {
$display = EntityViewDisplay::create(array(
'targetEntityType' => $group->entity_type,
'bundle' => $group->bundle,
'mode' => $group->mode,
))
->setStatus(TRUE);
}
}
if (isset($display)) {
$data = (array) $group;
unset($data['group_name'], $data['entity_type'], $data['bundle'], $data['mode'], $data['form'], $data['context']);
$display
->setThirdPartySetting('field_group', $group->group_name, $data);
$display
->save();
}
return $display;
}
/**
* Delete a field group.
*
* @param $group
* A group definition.
*/
function field_group_group_delete($group) {
if ($group->context == 'form') {
$display = EntityFormDisplay::load($group->entity_type . '.' . $group->bundle . '.' . $group->mode);
}
elseif ($group->context == 'view') {
$display = EntityViewDisplay::load($group->entity_type . '.' . $group->bundle . '.' . $group->mode);
}
/**
* @var $display \Drupal\Core\Entity\Display\EntityDisplayInterface
*/
if (isset($display)) {
$display
->unsetThirdPartySetting('field_group', $group->group_name);
$display
->save();
}
Drupal::moduleHandler()
->invokeAll('field_group_delete_field_group', array(
$group,
));
}
/**
* Get all groups.
*
* @param $entity_type
* The name of the entity.
* @param $bundle
* The name of the bundle.
* @param $context
* The context of the view mode (form or view)
* @param $mode
* The view mode.
*/
function field_group_info_groups($entity_type, $bundle, $context, $mode) {
if ($context == 'form') {
$display = EntityFormDisplay::load($entity_type . '.' . $bundle . '.' . $mode);
if (!$display) {
return array();
}
$data = $display
->getThirdPartySettings('field_group');
}
if ($context == 'view') {
$display = EntityViewDisplay::load($entity_type . '.' . $bundle . '.' . $mode);
if (!$display) {
return array();
}
$data = $display
->getThirdPartySettings('field_group');
}
$groups = array();
if (isset($data) && is_array($data)) {
foreach ($data as $group_name => $definition) {
$definition += array(
'group_name' => $group_name,
'entity_type' => $entity_type,
'bundle' => $bundle,
'context' => $context,
'mode' => $mode,
);
$groups[$group_name] = (object) $definition;
}
}
return $groups;
}
/**
* Loads a group definition.
*
* @param $group_name
* The name of the group.
* @param $entity_type
* The name of the entity.
* @param $bundle
* The name of the bundle.
* @param $context
* The context of the view mode (form or view)
* @param $mode
* The view mode to load.
*/
function field_group_load_field_group($group_name, $entity_type, $bundle, $context, $mode) {
$groups = field_group_info_groups($entity_type, $bundle, $context, $mode);
if (isset($groups[$group_name])) {
return $groups[$group_name];
}
}
/**
* Checks if a field_group exists in required context.
*
* @param String $group_name
* The name of the group.
* @param String $entity_type
* The name of the entity.
* @param String $bundle
* The bundle for the entity.
* @param $context
* The context of the view mode (form or view)
* @param String $mode
* The view mode context the group will be rendered.
*/
function field_group_exists($group_name, $entity_type, $bundle, $context, $mode) {
return (bool) field_group_load_field_group($group_name, $entity_type, $bundle, $context, $mode);
}
/**
* Remove empty groups on forms.
*
* @param String $parent_name
* The name of the element.
* @param array $element
* The element to check the empty state.
* @param array $groups
* Array of group objects.
*/
function field_group_remove_empty_form_groups($name, &$element, $groups, &$form_groups, $entity) {
$exceptions = array(
'user__account',
'comment__author',
);
$children = Element::children($element);
$hasChildren = FALSE;
if (count($children)) {
foreach ($children as $childname) {
if (in_array($childname, $groups)) {
field_group_remove_empty_form_groups($childname, $element[$childname], $groups, $form_groups, $entity);
}
$exception = $entity . '__' . $childname;
$hasChildren = $hasChildren ? TRUE : isset($element[$childname]['#type']) || isset($element[$childname]['#markup']) || in_array($exception, $exceptions);
}
}
if (!$hasChildren) {
// Remove empty elements from the #fieldgroups.
if (empty($element) && isset($form_groups[$name]) && !is_array($form_groups[$name])) {
foreach ($form_groups as $group_name => $group) {
if (isset($group->children)) {
$group_children = array_flip($group->children);
if (isset($group_children[$name])) {
unset($form_groups[$group_name]->children[$group_children[$name]]);
}
}
}
}
$element['#access'] = FALSE;
}
}
/**
* Remove empty groups on entity display.
*
* @param array $element
* The element to check the empty state.
* @param array $groups
* Array of group objects.
*/
function field_group_remove_empty_display_groups(&$element, $groups) {
$empty_child = TRUE;
$empty_group = TRUE;
// Loop through the visible children for current element.
foreach (Element::getVisibleChildren($element) as $name) {
// Descend if the child is a group.
if (in_array($name, $groups)) {
$empty_child = field_group_remove_empty_display_groups($element[$name], $groups);
if (!$empty_child) {
$empty_group = FALSE;
}
}
elseif (!empty($element[$name])) {
$clone_element = $element[$name];
// Weight parameter can make empty element seen as not empty.
unset($clone_element['#weight']);
if (!Element::isEmpty($clone_element)) {
$empty_group = FALSE;
}
}
}
// Reset an empty group.
if ($empty_group) {
$element = [];
}
return $empty_group;
}