Commit ef684feb by Manzar Hussain

add field group module

parent 2c441547
8.x-3.0-rc1, 2019-03-28
-----------------------
- View Mode '_custom' should not go through the entity_display_build_alter.
- Fix deprecated methods.
- Temp remove the typehinting.
- Fatal error when creating a paragraph.
- Use mb_* functions instead of deprecated Unicode::* methods.
- Large amount of fields causes tab functionality to break.
- Field_group_theme_suggestions_alter notices.
- Horizontal tab content is wrapped by detail border.
- Syntax error in HorizontalTabs.php causes module installation to fail.
- Duplicated fields with field_group on referenced ECK entities.
- Horizontal tabs break keyboard navigation.
- Accordion Doesn't Open on Error.
- Create field_group.api.php for D8 version.
- Choose sensible default tab for horizontal tabs.
- Extend signature of field_group_form_process().
- Migrated field groups all disabled.
- Empty fieldgroups are showing in forms.
- Missing hook_help.
- Missing UI for description text for field groups.
- Field groups default region should never be null.
- Revert "Issue #2991400 by DuaelFr: Field groups default region should never be null".
- Field groups default region should never be null.
- How to create horizontal tabs with 8.x-3.x ?.
- XSS patch horizontal-tabs.js.
- Allow modules to define form elements beneath field groups before they are created.
- Field_group_migrate.info.yml should not contain "version: VERSION".
- 2998205: Fix call to member function errors when the plugin was not found.
- PHP message: Error: Call to a member function process() on null.
- Set default values in migrate destinations plugins.
- D6 migration doesn't generate the migrations templates.
- Change package name of migrate sub-modules.
- Accordion/Default State doesn't do anything.
- Fix access check for empty groups.
- Fix config schema.
- Markup ID of each tab is not unique.
- Revert "Issue #2904577: Duplicate CSS ID confuses behat".
- Duplicate CSS ID confuses behat.
- The region part of entity view config isn't set for old installs.
- Remove extra param in call to field_group_info_groups.
- Update the processGroup implementations.
- Cannot declare class HtmlElement.
- Fix extending preRenderGroup.
- Coding standards.
- Remove helpers.inc.
- Invalid CSS ID for field group causes error.
- Add option to set group label classes for HTML element type.
- Fix migration tests.
- Fix migrate unit test + accordion.
- Getting d6_field_group plugin must define the source_module property Error When Using migrate-upgrade.
- Field groups are not compatible with field layout.
- Accordion items with children with errors not open.
- Accordion doesn't work.
- Fix syntax errors.
- Convert module to use short array syntax (new coding standard).
8.3.0-beta1, 2017-11-10
-------------------
- JS error: Modernizr is not defined.
- Add the new region property to the schema.
- Adding Multiple Fields wrapped by a Tabs Group cause maximum execution error.
- Branch tests are failing.
- .
- Creating Duplicate Fieldgroup Name Overwrites Existing Fieldgroup.
- Field groups are not compatible with field layout. Part 1: Make sure regions are changed when changing layout.
- Typo in Field Group Formatter Plugin HtmlElement::prerender.
- Revert "Issue #2846589 by huzooka: Typo in Field Group Formatter Plugin HtmlElement::prerender".
- Undefined index: form_display.
- Typo in Field Group Formatter Plugin HtmlElement::prerender.
- Replace all deprecated uses.
- MessageWarning: Invalid argument supplied for foreach() in field_group_info_groups() (line 663 of modules/contrib/field_group/field_group.module).
- Replace removed formBuilder->setError with formstate->setError.
- Undefined index: id in template_preprocess_fieldset() notice.
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Installation
* Configuration
* Maintainers
INTRODUCTION
------------
History:
Field_group was originally written when drupal 7 was released. For drupal 6,
the module is located in the CCK module (http://drupal.org/project/cck).
As drupal core has a fields API drupal > 6, the field_group module
is considered a contribution.
Description:
field_group is a module that will group a set of fields. In Drupal8,
with fields, one means all fields that come from fieldable entities.
You can add fieldgroups in several types with their own format settings.
field_group uses plugins to add your own formatter and rendering for
it.
One of the biggest improvements to previous versions, is that fieldgroups
have unlimited nesting, better display control.
Note that field_group will only group fields, it can not be used to hide
certain fields since this a permission matter.
Module project page:
http://drupal.org/project/field_group
Documentation page (D7 version):
http://drupal.org/node/1017838
http://drupal.org/node/1017962
Available group types:
- Html element
- Fieldsets
- Tabs (horizontal and vertical)
- Accordions
- Details (Use this if you want collapsible fieldsets)
- Details Sidebar
To submit bug reports and feature suggestions, or to track changes:
http://drupal.org/project/issues/field_group
REQUIREMENTS
------------
None.
INSTALLATION
------------
Install as you would normally install a contributed Drupal module. Visit:
https://www.drupal.org/documentation/install/modules-themes/modules-8
for further information.
CONFIGURATION
-------------
1. You can configure the field groups for different displays like, in
managed_form_display and managed_display of the entity type.
2. You can create different field groups under managed_form_display by
adding a new group under "Add new group" label and the format the
grouping using the desired formatter for displaying the same.
3. Same thing can be done in managed_display.
4. The field grouping done in managed display will be reflected on the
view detail page of the entity, while that done in the
managed_form_display will be reflected in the add/edit form of the entity.
-- Create field groups --
This section explains how to create groups of fields according to the type chosen.
- Fieldsets : This group of fields makes the internal content in a fieldset.
It is possible to add a title and a caption (which appears at
the bottom of the fieldset).
- Details : Similar to Fieldsets. You can configure them to be open (normal
fieldset) or collapsed.
- Details Sidebar: Similar to Details. You can configure them to be open
(normal fieldset) or collapsed and move them in the sidebar on
the node form.
- Html element : This fieldgroup renders the inner content in a HTML element.
You can configure attributes and label element.
The following two groupings works differently because you must associate them with
an other grouping.
- Accordions : This group of fields makes the child groups as a jQuery accordion.
As a first step you must create an Accordions group. You can set a
label and choose an effect. Then you can create an Accordion
Item as a child. This group can contain fields.
- Tabs : This fieldgroup renders child groups in its own tabs wrapper.
As a first step you must create an Tabs group. You can set
choose if you want that your tabs are show horizontally or vertically.
Then, you can create Tab as a child and choose one to be open by default.
This group can contain fields.
For all groups, you can add id or classes.
You can also choose if you want to mark a group as required if one of his fields is
require (except for Accordions and Tabs : you must passed by their children).
MAINTAINERS
-----------
stalski - http://drupal.org/user/322618
zuuperman - http://drupal.org/user/361625
swentel - http://drupal.org/user/107403
Inspirators:
yched - http://drupal.org/user/39567
{
"name": "drupal/field_group",
"description": "Provides the field_group module.",
"type": "drupal-module",
"license": "GPL-2.0-or-later",
"minimum-stability": "dev",
"require": {
"drupal/core": "^8.8 || ^9"
},
"require-dev": {
"drupal/jquery_ui_accordion": "^1.0"
},
"support": {
"issues": "https://www.drupal.org/project/issues/field_group",
"source": "https://git.drupalcode.org/project/field_group"
}
}
core.entity_view_display.*.*.*.third_party.field_group:
type: sequence
label: 'Field group settings on entity view'
sequence:
- type: mapping
label: A field group
mapping:
children:
type: sequence
label: 'The fields belonging to the group'
sequence:
- type: string
label: 'The field name'
label:
type: label
label: Readable name of the group
parent_name:
type: string
label: 'The parent group of this group'
region:
type: string
label: 'The region of this group'
weight:
type: integer
label: 'The weight of the group'
format_type:
type: string
label: 'The formatter of the group'
format_settings:
type: field_group.field_group_formatter_plugin.[%parent.format_type]
core.entity_form_display.*.*.*.third_party.field_group:
type: sequence
label: 'Field group settings on entity form'
sequence:
- type: mapping
label: A field group
mapping:
children:
type: sequence
label: 'The fields belonging to the group'
sequence:
- type: string
label: 'The field name'
label:
type: label
label: Readable name of the group
region:
type: string
label: 'The region of this group'
parent_name:
type: string
label: 'The parent group of this group'
weight:
type: integer
label: 'The weight of the group'
format_type:
type: string
label: 'The formatter of the group'
format_settings:
type: field_group.field_group_formatter_plugin.[%parent.format_type]
field_group.field_group_formatter_plugin.accordion:
type: field_group.field_group_formatter_plugin.base
label: 'Mapping for the accordion formatter settings'
mapping:
effect:
type: string
label: 'Effect on the accordion'
field_group.field_group_formatter_plugin.accordion_item:
type: field_group.field_group_formatter_plugin.base
label: 'Mapping for the accordion item formatter settings'
mapping:
formatter:
type: string
label: 'Formatting of the item'
description:
type: label
label: 'Description of the item'
required_fields:
type: boolean
label: 'Mark for required fields'
field_group.field_group_formatter_plugin.details:
type: field_group.field_group_formatter_plugin.base
label: 'Mapping for the details formatter settings'
mapping:
open:
type: boolean
label: 'Display element open by default.'
description:
type: text
label: 'Description of the element'
required_fields:
type: boolean
label: 'Mark for required fields'
field_group.field_group_formatter_plugin.details_sidebar:
type: field_group.field_group_formatter_plugin.details
label: 'Mapping for the details sidebar formatter settings'
mapping:
weight:
type: integer
label: 'Weight'
field_group.field_group_formatter_plugin.fieldset:
type: field_group.field_group_formatter_plugin.base
label: 'Mapping for the fieldset formatter settings'
mapping:
description:
type: label
label: 'Description of the item'
required_fields:
type: boolean
label: 'Mark for required fields'
field_group.field_group_formatter_plugin.html_element:
type: field_group.field_group_formatter_plugin.base
label: 'Mapping for the html element formatter settings'
mapping:
element:
type: string
label: 'html element tag to be used'
show_label:
type: boolean
label: 'show the label'
label_element:
type: string
label: 'html element tag to be used for the label'
label_element_classes:
type: string
label: 'html classes to be used for the label'
attributes:
type: string
label: 'html attributes for the element'
effect:
type: string
label: 'effect on the element'
speed:
type: string
label: 'speed of the effect'
required_fields:
type: boolean
label: 'Mark for required fields'
field_group.field_group_formatter_plugin.tab:
type: field_group.field_group_formatter_plugin.base
label: 'Mapping for the tab formatter settings'
mapping:
formatter:
type: string
label: 'default state for the tab'
description:
type: label
label: 'Description of the tab'
required_fields:
type: boolean
label: 'Mark for required fields'
field_group.field_group_formatter_plugin.tabs:
type: field_group.field_group_formatter_plugin.base
label: 'Mapping for the tab formatter settings'
mapping:
formatter:
type: string
label: 'default state for the tabs'
description:
type: label
label: 'description of the tabs'
required_fields:
type: boolean
label: 'Mark for required fields'
direction:
type: string
label: 'Direction of the tabs'
field_group.field_group_formatter_plugin.base:
type: mapping
label: 'Mapping for the base formatter settings'
mapping:
label:
type: label
label: 'Label of the fieldgroup'
classes:
type: string
label: 'Classes of the fieldgroup'
id:
type: string
label: 'Html id of the fieldgroup'
name: 'Field Group Migrate'
type: module
description: 'Provides the ability to migrate field groups from D6/D7 to D8.'
package: Migration
core_version_requirement: ^8.8 || ^9
dependencies:
- field_group:field_group
# Information added by Drupal.org packaging script on 2020-06-10
version: '8.x-3.1'
project: 'field_group'
datestamp: 1591772570
langcode: en
status: true
dependencies:
config:
- migrate.migration.d6_field_instance
module:
- field_group_migrate
- node
id: d6_field_group_entity_form_display
migration_tags:
- 'Drupal 6'
label: 'Field groups'
source:
plugin: d6_field_group
constants:
mode: entity_form_display
entity_type: node
form_mode: default
third_party_settings: { }
process:
mode: constants/mode
entity_type: constants/entity_type
bundle: type_name
form_mode: constants/form_mode
id:
plugin: concat
source:
- group_name
delimiter: .
field_group/label: label
field_group/weight: weight
field_group/children: children
field_group/format_type: converted_settings/format_type
field_group/format_settings: converted_settings/format_settings
destination:
plugin: field_group_entity_form_display
template: d6_field_instance_widget_settings
migration_dependencies:
required:
- d6_field_instance
migration_group: null
langcode: en
status: true
dependencies:
config:
- migrate.migration.d6_field_instance
module:
- field_group_migrate
- node
id: d6_field_group_entity_view_display
migration_tags:
- 'Drupal 6'
label: 'Field groups'
source:
plugin: d6_field_group
constants:
mode: entity_view_display
entity_type: node
third_party_settings: { }
process:
mode: constants/mode
entity_type: constants/entity_type
bundle: type_name
id:
plugin: concat
source:
- group_name
delimiter: .
field_group/label: label
field_group/weight: weight
field_group/children: children
field_group/format_type: converted_settings/format_type
field_group/format_settings: converted_settings/format_settings
destination:
plugin: field_group_entity_view_display
template: d6_field_instance_widget_settings
migration_dependencies:
required:
- d6_field_instance
migration_group: null
id: d7_field_group
label: Field groups
migration_tags:
- Drupal 7
source:
plugin: d7_field_group
process:
entity_type: entity_type
bundle: bundle
mode:
plugin: static_map
source: mode
bypass: true
map:
form: default
type:
plugin: static_map
source: mode
default_value: entity_view_display
map:
form: entity_form_display
group_name: group_name
settings: settings
destination:
plugin: d7_field_group
migration_dependencies:
required:
- d7_field_formatter_settings
<?php
namespace Drupal\field_group_migrate\Plugin\migrate\destination;
use Drupal\migrate\Plugin\migrate\destination\PerComponentEntityFormDisplay;
use Drupal\migrate\Row;
/**
* This class imports one field_group of an entity form display.
*
* @MigrateDestination(
* id = "field_group_entity_form_display"
* )
*/
class FieldGroupEntityFormDisplay extends PerComponentEntityFormDisplay {
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
$values = [];
// array_intersect_key() won't work because the order is important because
// this is also the return value.
foreach (array_keys($this->getIds()) as $id) {
$values[$id] = $row->getDestinationProperty($id);
}
$entity = $this->getEntity($values['entity_type'], $values['bundle'], $values[static::MODE_NAME]);
if (!$entity->isNew()) {
$settings = $row->getDestinationProperty('field_group');
$settings += [
'region' => 'content',
'parent_name' => '',
];
$entity->setThirdPartySetting('field_group', $row->getDestinationProperty('id'), $settings);
if (isset($settings['format_type']) && ($settings['format_type'] == 'no_style' || $settings['format_type'] == 'hidden')) {
$entity->unsetThirdPartySetting('field_group', $row->getDestinationProperty('id'));
}
$entity->save();
}
return array_values($values);
}
}
<?php
namespace Drupal\field_group_migrate\Plugin\migrate\destination;
use Drupal\migrate\Plugin\migrate\destination\PerComponentEntityDisplay;
use Drupal\migrate\Row;
/**
* This class imports one field_group of an entity form display.
*
* @MigrateDestination(
* id = "field_group_entity_view_display"
* )
*/
class FieldGroupEntityViewDisplay extends PerComponentEntityDisplay {
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
$values = [];
// array_intersect_key() won't work because the order is important because
// this is also the return value.
foreach (array_keys($this->getIds()) as $id) {
$values[$id] = $row->getDestinationProperty($id);
}
foreach ($row->getSourceProperty('view_modes') as $view_mode => $settings) {
$entity = $this->getEntity($values['entity_type'], $values['bundle'], $view_mode);
if (!$entity->isNew()) {
$settings += [
'region' => 'content',
'parent_name' => '',
];
$settings = array_merge($row->getDestinationProperty('field_group'), $settings);
$entity->setThirdPartySetting('field_group', $row->getDestinationProperty('id'), $settings);
if (isset($settings['format_type']) && ($settings['format_type'] == 'no_style' || $settings['format_type'] == 'hidden')) {
$entity->unsetThirdPartySetting('field_group', $row->getDestinationProperty('id'));
}
$entity->save();
}
}
return array_values($values);
}
}
<?php
namespace Drupal\field_group_migrate\Plugin\migrate\destination\d7;
use Drupal\migrate\Plugin\migrate\destination\DestinationBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* This class imports one field_group of an entity form display.
*
* @MigrateDestination(
* id = "d7_field_group"
* )
*/
class FieldGroup extends DestinationBase {
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
$values = [];
// array_intersect_key() won't work because the order is important because
// this is also the return value.
foreach (array_keys($this->getIds()) as $id) {
$values[$id] = $row->getDestinationProperty($id);
}
$entity = $this->getEntity($values['entity_type'], $values['bundle'], $values['mode'], $values['type']);
if (!$entity->isNew()) {
$settings = $row->getDestinationProperty('settings');
$settings += [
'region' => 'content',
];
$entity->setThirdPartySetting('field_group', $row->getDestinationProperty('group_name'), $settings);
if (isset($settings['format_type']) && ($settings['format_type'] == 'hidden')) {
$entity->unsetThirdPartySetting('field_group', $row->getDestinationProperty('group_name'));
}
$entity->save();
}
return array_values($values);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['entity_type']['type'] = 'string';
$ids['bundle']['type'] = 'string';
$ids['mode']['type'] = 'string';
$ids['type']['type'] = 'string';
$ids['group_name']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
$entity = $this->getEntity($destination_identifier['entity_type'], $destination_identifier['bundle'], $destination_identifier['mode'], $destination_identifier['type']);
if (!$entity->isNew()) {
$entity->unsetThirdPartySetting('field_group', $destination_identifier['group_name']);
$entity->save();
}
}
/**
* {@inheritdoc}
*/
public function fields(MigrationInterface $migration = NULL) {
// This is intentionally left empty.
}
/**
* Gets the entity.
*
* @param string $entity_type
* The entity type to retrieve.
* @param string $bundle
* The entity bundle.
* @param string $mode
* The display mode.
* @param string $type
* The destination type.
*
* @return \Drupal\Core\Entity\Display\EntityDisplayInterface
* The entity display object.
*/
protected function getEntity($entity_type, $bundle, $mode, $type) {
$function = $type == 'entity_form_display' ? 'getFormDisplay' : 'getViewDisplay';
return \Drupal::service('entity_display.repository')->$function($entity_type, $bundle, $mode);
}
}
<?php
namespace Drupal\field_group_migrate\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 field_group source.
*
* @MigrateSource(
* id = "d6_field_group",
* source_module = "fieldgroup",
* destination_module = "field_group"
* )
*/
class FieldGroup extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('content_group', 'g')
->fields('g', [
'group_type',
'type_name',
'group_name',
'label',
'settings',
'weight',
]);
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$query = $this->select('content_group_fields', 'f');
$query->fields('f', ['field_name'])
->condition('type_name', $row->getSourceProperty('type_name'))
->condition('group_name', $row->getSourceProperty('group_name'));
$fields = $query->execute()->fetchCol();
$row->setSourceProperty('children', $fields);
$row->setSourceProperty('settings', unserialize($row->getSourceProperty('settings')));
switch ($row->getSourceProperty('constants/mode')) {
case 'entity_form_display':
$this->transformEntityFormDisplaySettings($row);
break;
case 'entity_view_display':
$this->transformEntityViewDisplaySettings($row);
break;
}
return parent::prepareRow($row);
}
/**
*
*/
protected function transformEntityFormDisplaySettings(Row $row) {
$row->setSourceProperty('extracted_settings', $row->getSourceProperty('settings/form'));
$source_settings = $row->getSourceProperty('extracted_settings');
$settings = [
'format_type' => 'details',
'format_settings' => [],
];
switch ($source_settings['style']) {
case 'no_style':
$settings['format_type'] = 'no_style';
break;
case 'simple':
$settings['format_type'] = 'html_element';
$settings['format_settings']['element'] = 'div';
$settings['format_settings']['label_element'] = 'h2';
break;
case 'fieldset':
$settings['format_type'] = 'fieldset';
break;
case 'fieldset_collapsible':
$settings['format_type'] = 'details';
$settings['format_settings']['open'] = TRUE;
break;
case 'fieldset_collapsed':
$settings['format_type'] = 'details';
$settings['format_settings']['open'] = FALSE;
break;
case 'hidden':
$settings['format_type'] = 'hidden';
break;
}
$row->setSourceProperty('converted_settings', $settings);
}
/**
*
*/
protected function transformEntityViewDisplaySettings(Row $row) {
$row->setSourceProperty('extracted_settings', $row->getSourceProperty('settings/display'));
$view_modes = array_diff(array_keys($row->getSourceProperty('extracted_settings')), ['label', 'description', 'weight']);
$view_modes = array_filter($view_modes, function ($value) {
return !is_numeric($value);
});
$row->setSourceProperty('view_mode_keys', $view_modes);
$view_modes = [];
foreach ($row->getSourceProperty('view_mode_keys') as $view_mode) {
$source_settings = $row->getSourceProperty('extracted_settings/' . $view_mode);
$row->setSourceProperty('view_modes', []);
$settings = [
'format_type' => 'details',
'format_settings' => [],
];
switch ($source_settings['format']) {
case 'no_style':
$settings['format_type'] = 'no_style';
break;
case 'simple':
$settings['format_type'] = 'html_element';
$settings['format_settings']['element'] = 'div';
$settings['format_settings']['label_element'] = 'h2';
break;
case 'fieldset':
$settings['format_type'] = 'fieldset';
break;
case 'fieldset_collapsible':
$settings['format_type'] = 'details';
$settings['format_settings']['open'] = TRUE;
break;
case 'fieldset_collapsed':
$settings['format_type'] = 'details';
$settings['format_settings']['open'] = FALSE;
break;
case 'hidden':
$settings['format_type'] = 'hidden';
break;
}
/**
* @todo: ?
*/
if ($view_mode == 'full') {
$view_mode = 'default';
}
// $row->setSourceProperty('view_modes/' . $view_mode, $settings);.
$view_modes[$view_mode] = $settings;
}
$row->setSourceProperty('view_modes', $view_modes);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['type_name']['type'] = 'string';
$ids['type_name']['alias'] = 'g';
$ids['group_name']['type'] = 'string';
$ids['group_name']['alias'] = 'g';
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'group_type',
'type_name',
'group_name',
'label',
'settings',
'weight',
];
return array_combine($fields, $fields);
}
}
<?php
namespace Drupal\field_group_migrate\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 7 field_group source.
*
* @MigrateSource(
* id = "d7_field_group",
* source_module = "field_group",
* destination_module = "field_group"
* )
*/
class FieldGroup extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('field_group', 'f')->fields('f');
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$data = unserialize($row->getSourceProperty('data'));
$format_settings = $data['format_settings'] + $data['format_settings']['instance_settings'];
unset($format_settings['instance_settings']);
$settings = [
'children' => $data['children'],
'parent_name' => $row->getSourceProperty('parent_name'),
'weight' => $data['weight'],
'label' => $data['label'],
'format_settings' => $format_settings,
'format_type' => $data['format_type'],
'region' => 'content',
];
switch ($data['format_type']) {
case 'div':
$settings['format_type'] = 'html_element';
$settings['format_settings']['element'] = 'div';
break;
case 'tabs':
$settings['format_type'] = 'tabs';
$settings['format_settings']['direction'] = 'vertical';
break;
case 'htabs':
$settings['format_type'] = 'tabs';
$settings['format_settings']['direction'] = 'horizontal';
break;
case 'htab':
$settings['format_type'] = 'tab';
break;
case 'multipage-group':
// @todo Check if there is a better way to deal with this format type.
$settings['format_type'] = 'tabs';
break;
case 'multipage':
// @todo Check if there is a better way to deal with this format type.
$settings['format_type'] = 'tab';
break;
}
$row->setSourceProperty('settings', $settings);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['id']['type'] = 'integer';
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'id' => $this->t('ID'),
'identifier' => $this->t('Identifier'),
'group_name' => $this->t('Group name'),
'entity_type' => $this->t('Entity type'),
'bundle' => $this->t('Bundle'),
'mode' => $this->t('View mode'),
'parent_name' => $this->t('Parent name'),
'region' => $this->t('Region'),
'data' => $this->t('Data'),
];
return $fields;
}
}
<?php
/**
* @file
* A database agnostic dump for testing purposes.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$connection->schema()->createTable('field_group', [
'fields' => [
'id' => [
'type' => 'serial',
'not null' => TRUE,
'size' => 'normal',
],
'identifier' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '255',
'default' => '',
],
'group_name' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '32',
'default' => '',
],
'entity_type' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '32',
'default' => '',
],
'bundle' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '128',
'default' => '',
],
'mode' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '128',
'default' => '',
],
'parent_name' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '32',
'default' => '',
],
'data' => [
'type' => 'blob',
'not null' => TRUE,
'size' => 'big',
],
],
'primary key' => [
'id',
],
'unique keys' => [
'identifier' => [
'identifier',
],
],
'indexes' => [
'group_name' => [
'group_name',
],
],
'mysql_character_set' => 'utf8',
]);
$connection->insert('field_group')
->fields([
'id',
'identifier',
'group_name',
'entity_type',
'bundle',
'mode',
'parent_name',
'data',
])
->values([
'id' => '1',
'identifier' => 'group_page|node|page|default',
'group_name' => 'group_page',
'entity_type' => 'node',
'bundle' => 'page',
'mode' => 'default',
'parent_name' => '',
'data' => 'a:5:{s:5:"label";s:10:"Node group";s:6:"weight";i:0;s:8:"children";a:0:{}s:11:"format_type";s:5:"htabs";s:15:"format_settings";a:1:{s:17:"instance_settings";a:0:{}}}',
])
->values([
'id' => '2',
'identifier' => 'group_user|user|user|default',
'group_name' => 'group_user',
'entity_type' => 'user',
'bundle' => 'user',
'mode' => 'default',
'parent_name' => '',
'data' => 'a:5:{s:5:"label";s:17:"User group parent";s:6:"weight";i:1;s:8:"children";a:0:{}s:11:"format_type";s:3:"div";s:15:"format_settings";a:1:{s:17:"instance_settings";a:0:{}}}',
])
->values([
'id' => '3',
'identifier' => 'group_user_child|user|user|default',
'group_name' => 'group_user_child',
'entity_type' => 'user',
'bundle' => 'user',
'mode' => 'default',
'parent_name' => 'group_user',
'data' => 'a:5:{s:5:"label";s:16:"User group child";s:6:"weight";i:99;s:8:"children";a:1:{i:0;s:12:"user_picture";}s:11:"format_type";s:4:"tabs";s:15:"format_settings";a:2:{s:5:"label";s:16:"User group child";s:17:"instance_settings";a:2:{s:7:"classes";s:16:"user-group-child";s:2:"id";s:33:"group_article_node_article_teaser";}}}',
])
->values([
'id' => '4',
'identifier' => 'group_article|node|article|teaser',
'group_name' => 'group_article',
'entity_type' => 'node',
'bundle' => 'article',
'mode' => 'teaser',
'parent_name' => '',
'data' => 'a:5:{s:5:"label";s:10:"htab group";s:6:"weight";i:2;s:8:"children";a:1:{i:0;s:11:"field_image";}s:11:"format_type";s:4:"htab";s:15:"format_settings";a:1:{s:17:"instance_settings";a:1:{s:7:"classes";s:10:"htab-group";}}}',
])
->values([
'id' => '5',
'identifier' => 'group_page|node|page|form',
'group_name' => 'group_page',
'entity_type' => 'node',
'bundle' => 'page',
'mode' => 'form',
'parent_name' => '',
'data' => 'a:5:{s:5:"label";s:15:"Node form group";s:6:"weight";i:0;s:8:"children";a:0:{}s:11:"format_type";s:5:"htabs";s:15:"format_settings";a:1:{s:17:"instance_settings";a:0:{}}}',
])
->values([
'id' => '6',
'identifier' => 'group_article|node|article|form',
'group_name' => 'group_article',
'entity_type' => 'node',
'bundle' => 'article',
'mode' => 'form',
'parent_name' => '',
'data' => 'a:5:{s:5:"label";s:15:"htab form group";s:6:"weight";i:2;s:8:"children";a:1:{i:0;s:11:"field_image";}s:11:"format_type";s:4:"htab";s:15:"format_settings";a:1:{s:17:"instance_settings";a:0:{}}}',
])
->execute();
$connection->insert('system')
->fields([
'filename',
'name',
'type',
'owner',
'status',
'bootstrap',
'schema_version',
'weight',
'info',
])
->values([
'filename' => 'sites/all/modules/field_group/field_group.module',
'name' => 'field_group',
'type' => 'module',
'owner' => '',
'status' => '1',
'bootstrap' => '0',
'schema_version' => '7008',
'weight' => '1',
'info' => 'a:12:{s:4:"name";s:11:"Field Group";s:11:"description";s:67:"Provides the ability to group your fields on both form and display.";s:7:"package";s:6:"Fields";s:12:"dependencies";a:2:{i:0;s:5:"field";i:1;s:6:"ctools";}s:4:"core";s:3:"7.x";s:5:"files";a:2:{i:0;s:25:"tests/field_group.ui.test";i:1;s:30:"tests/field_group.display.test";}s:7:"version";s:7:"7.x-1.5";s:7:"project";s:11:"field_group";s:9:"datestamp";s:10:"1452033709";s:5:"mtime";i:1486548096;s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}',
])
->execute();
<?php
namespace Drupal\Tests\field_group_migrate\Kernel\Migrate\d7;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests field group migration.
*
* @group field_group
*/
class MigrateFieldGroupTest extends MigrateDrupal7TestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'field_group',
'field_group_migrate',
'comment',
'datetime',
'image',
'link',
'node',
'taxonomy',
'telephone',
'text',
'taxonomy',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->loadFixture(__DIR__ . '/../../../../fixtures/drupal7.php');
$this->installConfig(static::$modules);
$this->executeMigrations([
'd7_node_type',
'd7_comment_type',
'd7_taxonomy_vocabulary',
'd7_view_modes',
'd7_field',
'd7_field_instance',
'd7_field_formatter_settings',
'd7_field_group',
]);
}
/**
* Asserts various aspects of a migrated field group.
*
* @param $id
* The id of the entity display to which the field group applies.
* @param $type
* The destination type.
* @param $group_name
* The name of the field group.
* @param $expected_label
* The expected label.
* @param int $expected_weight
* The expected label.
* @param array $expected_format_settings
* The expected format settings.
* @param string $expected_format_type
* The expected format type.
* @param array $expected_children
* The expected children.
* @param string $expected_parent_name
* The expected parent name.
*/
protected function assertEntity($id, $type, $group_name, $expected_label, $expected_weight = 0, $expected_format_settings = [], $expected_format_type = 'tabs', $expected_children = [], $expected_parent_name = '') {
/** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $entity */
$entity = \Drupal::entityTypeManager()
->getStorage($type)
->load($id);
$field_group_settings = $entity->getThirdPartySettings('field_group');
$this->assertNotEmpty($field_group_settings);
$this->assertArrayHasKey($group_name, $field_group_settings);
$field_group = $field_group_settings[$group_name];
$this->assertEquals($expected_label, $field_group['label']);
$this->assertEquals($expected_format_settings, $field_group['format_settings']);
$this->assertEquals($expected_children, $field_group['children']);
$this->assertEquals($expected_parent_name, $field_group['parent_name']);
$this->assertEquals($expected_weight, $field_group['weight']);
$this->assertEquals($expected_format_type, $field_group['format_type']);
}
/**
* Test field group migration from Drupal 7 to 8.
*/
public function testFieldGroup() {
$this->assertEntity('node.page.default', 'entity_view_display', 'group_page', 'Node group', 0, ['direction' => 'horizontal']);
$this->assertEntity('user.user.default', 'entity_view_display', 'group_user', 'User group parent', 1, ['element' => 'div'], 'html_element');
$this->assertEntity('user.user.default', 'entity_view_display', 'group_user_child', 'User group child', 99, ['direction' => 'vertical', 'label' => 'User group child', 'classes' => 'user-group-child', 'id' => 'group_article_node_article_teaser'], 'tabs', ['user_picture'], 'group_user');
$this->assertEntity('node.article.teaser', 'entity_view_display', 'group_article', 'htab group', 2, ['classes' => 'htab-group'], 'tab', ['field_image']);
// Check an entity_view_display without a field group.
/** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $entity */
$entity = \Drupal::entityTypeManager()
->getStorage('entity_view_display')
->load('node.page.teaser');
$field_group_settings = $entity->getThirdPartySettings('field_group');
$this->assertEmpty($field_group_settings);
$this->assertEntity('node.page.default', 'entity_form_display', 'group_page', 'Node form group', 0, ['direction' => 'horizontal']);
$this->assertEntity('node.article.default', 'entity_form_display', 'group_article', 'htab form group', 2, [], 'tab', ['field_image']);
// Check an entity_form_display without a field group.
$entity = \Drupal::entityTypeManager()
->getStorage('entity_form_display')
->load('node.blog.default');
$field_group_settings = $entity->getThirdPartySettings('field_group');
$this->assertEmpty($field_group_settings);
}
}
<?php
namespace Drupal\Tests\field_group_migrate\Unit\Migrate\d7;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests D7 field group source plugin.
*
* @group field_group
*/
class FieldGroupTest extends MigrateSqlSourceTestBase {
const PLUGIN_CLASS = 'Drupal\field_group_migrate\Plugin\migrate\source\d7\FieldGroup';
protected $migrationConfiguration = [
'id' => 'test',
'source' => [
'plugin' => 'd7_field_group',
],
];
protected $expectedResults = [
[
'id' => '1',
'identifier' => 'group_page|node|page|default',
'group_name' => 'group_page',
'entity_type' => 'node',
'bundle' => 'page',
'mode' => 'default',
'parent_name' => '',
'data' => 'a:5:{s:5:"label";s:10:"Node group";s:6:"weight";i:0;s:8:"children";a:0:{}s:11:"format_type";s:5:"htabs";s:15:"format_settings";a:1:{s:17:"instance_settings";a:0:{}}}',
],
[
'id' => '2',
'identifier' => 'group_user|user|user|default',
'group_name' => 'group_user',
'entity_type' => 'user',
'bundle' => 'user',
'mode' => 'default',
'parent_name' => '',
'data' => 'a:5:{s:5:"label";s:17:"User group parent";s:6:"weight";i:1;s:8:"children";a:0:{}s:11:"format_type";s:3:"div";s:15:"format_settings";a:1:{s:17:"instance_settings";a:0:{}}}',
],
[
'id' => '3',
'identifier' => 'group_user_child|user|user|default',
'group_name' => 'group_user_child',
'entity_type' => 'user',
'bundle' => 'user',
'mode' => 'default',
'parent_name' => 'group_user',
'data' => 'a:5:{s:5:"label";s:16:"User group child";s:6:"weight";i:99;s:8:"children";a:1:{i:0;s:12:"user_picture";}s:11:"format_type";s:4:"tabs";s:15:"format_settings";a:2:{s:5:"label";s:16:"User group child";s:17:"instance_settings";a:2:{s:7:"classes";s:16:"user-group-child";s:2:"id";s:33:"group_article_node_article_teaser";}}}',
],
[
'id' => '4',
'identifier' => 'group_article|node|article|teaser',
'group_name' => 'group_article',
'entity_type' => 'node',
'bundle' => 'article',
'mode' => 'teaser',
'parent_name' => '',
'data' => 'a:5:{s:5:"label";s:10:"htab group";s:6:"weight";i:2;s:8:"children";a:1:{i:0;s:11:"field_image";}s:11:"format_type";s:4:"htab";s:15:"format_settings";a:1:{s:17:"instance_settings";a:1:{s:7:"classes";s:10:"htab-group";}}}',
],
[
'id' => '5',
'identifier' => 'group_page|node|page|form',
'group_name' => 'group_page',
'entity_type' => 'node',
'bundle' => 'page',
'mode' => 'form',
'parent_name' => '',
'data' => 'a:5:{s:5:"label";s:15:"Node form group";s:6:"weight";i:0;s:8:"children";a:0:{}s:11:"format_type";s:5:"htabs";s:15:"format_settings";a:1:{s:17:"instance_settings";a:0:{}}}',
],
[
'id' => '6',
'identifier' => 'group_article|node|article|form',
'group_name' => 'group_article',
'entity_type' => 'node',
'bundle' => 'article',
'mode' => 'form',
'parent_name' => '',
'data' => 'a:5:{s:5:"label";s:15:"htab form group";s:6:"weight";i:2;s:8:"children";a:1:{i:0;s:11:"field_image";}s:11:"format_type";s:4:"htab";s:15:"format_settings";a:1:{s:17:"instance_settings";a:0:{}}}',
],
];
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->databaseContents['field_group'] = $this->expectedResults;
parent::setUp();
}
/**
* {@inheritdoc}
*/
public function providerSource() {
// @TODO FIX.
return [];
}
}
#field-overview tr.field-group .group-label,
#field-display-overview tr.field-group .group-label {
font-weight: bold;
}
#field-overview tr.static-region,
#field-display-overview tr.static-region {
background-color: #ddd;
}
#edit-refresh {
display: none;
}
<?php
/**
* @file
* Hooks for the field_group module.
*/
use Drupal\Core\Form\FormStateInterface;
/**
* @addtogroup hooks
* @{
*/
/**
* Pre render the build of the field group element.
*
* @param array $element
* Group being rendered.
* @param object $group
* The Field group info.
* @param object $rendering_object
* The entity / form being rendered.
*/
function hook_field_group_pre_render(array &$element, &$group, &$rendering_object) {
// Add all field_group format types to the js settings.
$element['#attached']['drupalSettings']['field_group'] = [
$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([
'format_type' => $group->format_type,
'configuration' => ['label' => $group->label, 'settings' => $group->format_settings],
'group' => $group,
]);
$plugin->preRender($element, $rendering_object);
}
/**
* Alter the pre_rendered build of the field group element.
*
* @param array $element
* Group being rendered.
* @param object $group
* The Field group info.
* @param object $rendering_object
* The entity / form being rendered.
*/
function hook_field_group_pre_render_alter(array &$element, &$group, &$rendering_object) {
if ($group->format_type == 'htab') {
$element['#theme_wrappers'] = [
'container' => [
'#attributes' => ['class' => 'foobar'],
],
];
}
}
/**
* Alter the pre_rendered build of the entity view.
*
* @param array $element
* Group being rendered.
*/
function hook_field_group_build_pre_render_alter(array &$element) {
$element['#fieldgroups']['my_group']['region'] = 'new_region';
}
/**
* Process the field group.
*
* @param array $element
* The element being processed.
* @param $group
* The group info.
* @param $complete_form
* The complete form.
*/
function hook_field_group_form_process(array &$element, &$group, &$complete_form) {
$element['#states'] = [
'visible' => [
':input[name="field_are_you_ok"]' => ['value' => 'yes'],
],
];
}
/**
* Alter the processed build of the group.
*
* @param array $element
* The element being processed.
* @param $group
* The group info.
* @param $complete_form
* The complete form.
*/
function hook_field_group_form_process_alter(array &$element, &$group, &$complete_form) {
$element['#states'] = [
'visible' => [
':input[name="field_are_you_ok"]' => ['value' => 'yes'],
],
];
}
/**
* Alter the form after all groups are processed.
*
* @param array $element
* The element being processed.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state
* @param $complete_form
* The complete form.
*/
function hook_field_group_form_process_build_alter(array &$element, FormStateInterface $form_state, &$complete_form) {
$element['group_example']['#states'] = [
'visible' => [
':input[name="field_are_you_ok"]' => ['value' => 'yes'],
],
];
}
/**
* Hook into the deletion event of a fieldgroup.
*
* @param $group
* The deleted group.
*/
function hook_field_group_delete_field_group($group) {
// Extra cleanup code.
}
/**
* @} End of "addtogroup hooks".
*/
name: 'Field Group'
type: module
description: 'Provides the ability to group your fields on both form and display.'
package : Fields
core_version_requirement: ^8.8 || ^9
dependencies:
- drupal:field
# Information added by Drupal.org packaging script on 2020-06-10
version: '8.x-3.1'
project: 'field_group'
datestamp: 1591772570
<?php
/**
* @file
* Update hooks for the Field Group module.
*/
/**
* Implements hook_requirements().
*/
function field_group_requirements($phase) {
$requirements = [];
if ($phase == 'runtime') {
// Check jQuery UI Accordion module for D9.
if (version_compare(\Drupal::VERSION, 9) > 0) {
if (!\Drupal::moduleHandler()->moduleExists('jquery_ui_accordion')) {
$requirements['field_group_jquery_ui_accordion'] = [
'title' => t('Field Group'),
'value' => t('jQuery UI Accordion not enabled'),
'description' => t('If you want to use the Field Group accordion formatter, you will need to install the <a href=":link" target="_blank">jQuery UI Accordion</a> module.', [':link' => 'https://www.drupal.org/project/jquery_ui_accordion']),
'severity' => REQUIREMENT_WARNING,
];
}
else {
$requirements['field_group_jquery_ui_accordion'] = [
'title' => t('Field Group'),
'description' => t('The jQuery UI Accordion module is installed'),
'severity' => REQUIREMENT_INFO,
];
}
}
}
return $requirements;
}
/**
* Removed in favor of hook_post_update script.
*/
function field_group_update_8301() {
// @see field_group_post_update_0001().
}
/**
* Install the 'jquery_ui_accordion' module if it exists.
*/
function field_group_update_8302() {
try {
// Enables the jQuery UI accordion module if it exists.
if (\Drupal::service('extension.list.module')
->getName('jquery_ui_accordion')) {
\Drupal::service('module_installer')
->install(['jquery_ui_accordion'], FALSE);
return t('The "jquery_ui_accordion" module has been installed.');
}
}
catch (\Exception $e) {
return
t('If you want to use the Field Group accordion formatter, you will need to install the <a href=":link" target="_blank">jQuery UI Accordion</a> module.',
[':link' => 'https://www.drupal.org/project/jquery_ui_accordion']);
}
}
field_ui:
version: VERSION
js:
js/field_group.field_ui.js: {}
css:
component:
css/field_group.field_ui.css: {}
dependencies:
- core/jquery
- core/jquery.once
- core/drupal
- core/drupalSettings
core:
version: VERSION
js:
js/field_group.js: {}
dependencies:
- core/jquery
- core/jquery.once
- core/drupal
- core/drupalSettings
formatter.accordion:
version: VERSION
js:
formatters/accordion/accordion.js: {}
dependencies:
- core/jquery.ui.accordion
formatter.html_element:
version: VERSION
js:
formatters/html_element/html-element.js: {}
formatter.fieldset:
version: VERSION
js:
formatters/fieldset/fieldset.js: {}
formatter.details:
version: VERSION
js:
formatters/details/details.js: {}
formatter.tabs:
version: VERSION
js:
formatters/tabs/tabs.js: {}
dependencies:
- core/modernizr
element.horizontal_tabs:
version: VERSION
js:
# Load before field_group/core.
formatters/tabs/horizontal-tabs.js: {weight: -1}
css:
component:
formatters/tabs/horizontal-tabs.css: {}
dependencies:
- core/drupal.collapse
- core/modernizr
field_group.field_group_add:
class: \Drupal\Core\Menu\LocalActionDefault
deriver: \Drupal\field_group\Plugin\Derivative\FieldGroupLocalAction
<?php
/**
* @file
* Post update functions for Field Group.
*/
/**
* Assign a region to Field Groups.
*/
function field_group_post_update_0001() {
foreach (['entity_form_display', 'entity_view_display'] as $entity_type) {
foreach (\Drupal::entityTypeManager()->getStorage($entity_type)->loadMultiple() as $display) {
/** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $display */
if (in_array('field_group', $display->getThirdPartyProviders())) {
$updated = FALSE;
// Take Display Suite regions into account.
$has_ds = FALSE;
$ds_regions = [];
if ($entity_type == 'entity_view_display' && in_array('ds', $display->getThirdPartyProviders())) {
$ds = $display->getThirdPartySettings('ds');
if (!empty($ds['regions'])) {
foreach ($ds['regions'] as $region_name => $region_fields) {
foreach ($region_fields as $field_name) {
$has_ds = TRUE;
$ds_regions[$field_name] = $region_name;
}
}
}
}
$field_groups = $display->getThirdPartySettings('field_group');
foreach ($field_groups as $group_name => $data) {
if (!isset($data['region'])) {
$region = 'content';
if ($has_ds) {
$region = 'hidden';
if (isset($ds_regions[$group_name])) {
$region = $ds_regions[$group_name];
}
}
$data['region'] = $region;
$display->setThirdPartySetting('field_group', $group_name, $data);
$updated = TRUE;
}
}
if ($updated) {
$display->save();
}
}
}
}
}
services:
plugin.manager.field_group.formatters:
class: Drupal\field_group\FieldGroupFormatterPluginManager
parent: default_plugin_manager
field_group.subscriber:
class: Drupal\field_group\Routing\RouteSubscriber
arguments: ['@entity_type.manager']
tags:
- { name: event_subscriber }
field_group.param_converter:
class: Drupal\field_group\Routing\FieldGroupConverter
tags:
- { name: paramconverter }
(function ($) {
'use strict';
Drupal.FieldGroup = Drupal.FieldGroup || {};
Drupal.FieldGroup.Effects = Drupal.FieldGroup.Effects || {};
/**
* Implements Drupal.FieldGroup.processHook().
*/
Drupal.FieldGroup.Effects.processAccordion = {
execute: function (context, settings, group_info) {
$('div.field-group-accordion-wrapper', context).once('fieldgroup-effects').each(function () {
var wrapper = $(this);
// Get the index to set active.
var active_index = false;
wrapper.find('.accordion-item').each(function (i) {
if ($(this).hasClass('field-group-accordion-active')) {
active_index = i;
}
});
wrapper.accordion({
heightStyle: 'content',
active: active_index,
collapsible: true,
changestart: function (event, ui) {
if ($(this).hasClass('effect-none')) {
ui.options.animated = false;
}
else {
ui.options.animated = 'slide';
}
}
});
if (group_info.context === 'form') {
var $firstErrorItem = false;
// Add required fields mark to any element containing required fields
wrapper.find('div.field-group-accordion-item').each(function (i) {
var $this = $(this);
if ($this.is('.required-fields') && ($this.find('[required]').length > 0 || $this.find('.form-required').length > 0)) {
$('h3.ui-accordion-header a').eq(i).addClass('form-required');
}
if ($('.error', $this).length) {
// Save first error item, for focussing it.
if (!$firstErrorItem) {
$firstErrorItem = $this.parent().accordion('option', 'active', i);
}
$('h3.ui-accordion-header').eq(i).addClass('error');
}
});
// Save first error item, for focussing it.
if (!$firstErrorItem) {
$('.ui-accordion-content-active', $firstErrorItem).css({height: 'auto', width: 'auto', display: 'block'});
}
}
});
}
};
})(jQuery);
(function ($) {
'use strict';
Drupal.FieldGroup = Drupal.FieldGroup || {};
Drupal.FieldGroup.Effects = Drupal.FieldGroup.Effects || {};
/**
* This script adds the required and error classes to the details wrapper.
*/
Drupal.behaviors.fieldGroupDetails = {
attach: function (context) {
$(context).find('.field-group-details').once('field-group-details').each(function () {
var $this = $(this);
if ($this.is('.required-fields') && ($this.find('[required]').length > 0 || $this.find('.form-required').length > 0)) {
$('summary', $this).first().addClass('form-required');
}
});
}
};
})(jQuery);
(function ($) {
'use strict';
Drupal.FieldGroup = Drupal.FieldGroup || {};
Drupal.FieldGroup.Effects = Drupal.FieldGroup.Effects || {};
/**
* This script adds the required and error classes to the fieldset wrapper.
*/
Drupal.behaviors.fieldGroupDFieldset = {
attach: function (context) {
$(context).find('.field-group-fieldset').once('field-group-fieldset').each(function () {
var $this = $(this);
if ($this.is('.required-fields') && ($this.find('[required]').length > 0 || $this.find('.form-required').length > 0)) {
$('legend', $this).first().addClass('form-required');
}
});
}
};
})(jQuery);
(function ($) {
'use strict';
Drupal.FieldGroup = Drupal.FieldGroup || {};
Drupal.FieldGroup.Effects = Drupal.FieldGroup.Effects || {};
/**
* Implements Drupal.FieldGroup.processHook().
*/
Drupal.FieldGroup.Effects.processHtml_element = {
execute: function (context, settings, group_info) {
$('.field-group-html-element', context).once('fieldgroup-effects').each(function () {
var $wrapper = $(this);
if ($wrapper.hasClass('fieldgroup-collapsible')) {
Drupal.FieldGroup.Effects.processHtml_element.renderCollapsible($wrapper);
}
else {
// Add required field markers if needed
if (group_info.settings.show_label && $wrapper.is('.required-fields') && ($wrapper.find('[required]').length > 0 || $wrapper.find('.form-required').length > 0)) {
$wrapper.find(group_info.settings.label_element + ':first').addClass('form-required');
}
}
});
},
renderCollapsible: function($wrapper) {
// Turn the legend into a clickable link, but retain span.field-group-format-toggler
// for CSS positioning.
var $toggler = $('.field-group-toggler:first', $wrapper);
var $link = $('<a class="field-group-title" href="#"></a>');
$link.prepend($toggler.contents());
// Add required field markers if needed
if ($wrapper.is('.required-fields') && ($wrapper.find('[required]').length > 0 || $wrapper.find('.form-required').length > 0)) {
$link.addClass('form-required');
}
$link.appendTo($toggler);
// .wrapInner() does not retain bound events.
$link.click(function () {
var wrapper = $wrapper.get(0);
// Don't animate multiple times.
if (!wrapper.animating) {
wrapper.animating = true;
var speed = $wrapper.hasClass('speed-fast') ? 300 : 1000;
if ($wrapper.hasClass('effect-none') && $wrapper.hasClass('speed-none')) {
$('> .field-group-wrapper', wrapper).toggle();
}
else if ($wrapper.hasClass('effect-blind')) {
$('> .field-group-wrapper', wrapper).toggle('blind', {}, speed);
}
else {
$('> .field-group-wrapper', wrapper).toggle(speed);
}
wrapper.animating = false;
}
$wrapper.toggleClass('collapsed');
return false;
});
}
};
})(jQuery);
.horizontal-tabs {
margin: 0 0 1em 0; /* LTR */
padding: 0;
border: 1px solid #ccc;
position: relative; /* IE6/7 */
}
[dir="rtl"] .horizontal-tabs {
margin: 0 0 1em 0;
}
.horizontal-tabs .horizontal-tabs-list {
display: inline-block;
margin: 0;
border: 0;
padding: 0;
list-style: none;
background-color: #eee;
border-bottom: 1px solid #ccc; /* LTR */
width: 100%;
height: auto;
clear: both;
}
[dir="rtl"] .horizontal-tabs .horizontal-tabs-list {
border-right: 0;
border-left: 1px solid #dedede;
}
.horizontal-tabs-panes .horizontal-tabs-pane {
padding: 0 1em;
border: 0;
background-color: unset;
box-shadow: unset;
}
.horizontal-tabs-pane > summary {
display: none;
}
/* Layout of each tab */
.horizontal-tabs .horizontal-tab-button {
background: #eee;
border-right: 1px solid #ccc; /* LTR */
padding-top: 0;
margin: 0;
min-width: 5em; /* IE7 */
float: left; /* LTR */
}
[dir="rtl"] .horizontal-tabs .horizontal-tab-button {
border-right: 0;
border-left: 1px solid #ccc;
float: right;
}
.horizontal-tabs .horizontal-tab-button a {
display: block;
text-decoration: none;
padding: 0.5em 0.6em;
}
.horizontal-tabs .horizontal-tab-button a:hover {
outline: none;
background-color: #fff;
}
.horizontal-tabs .horizontal-tab-button li:hover,
.horizontal-tabs .horizontal-tab-button li:focus {
background-color: #ddd;
}
.horizontal-tabs ul.horizontal-tabs-list :focus {
outline: none;
}
.horizontal-tab-button a:focus strong,
.horizontal-tab-button a:active strong,
.horizontal-tab-button a:hover strong {
text-decoration: none;
outline: none;
}
.horizontal-tab-button.selected {
background-color: #fff;
border-bottom: 1px solid #fff;
margin-bottom: -1px;
}
[dir="rtl"] .horizontal-tab-button.selected {
border-left-width: 0;
border-right-width: 1px;
}
.horizontal-tabs ul.horizontal-tabs-list li a,
.horizontal-tabs ul.horizontal-tabs-list li.selected a {
display: block;
text-decoration: none;
padding: 0.6em 1em;
position: relative;
top: 0;
}
.horizontal-tab-button .selected strong {
color: #000;
}
.horizontal-tab-button .summary {
display: block;
}
.horizontal-tab-button .summary {
line-height: normal;
margin-bottom: 0;
}
/**
* tab content
*/
div.field-group-htabs-wrapper .field-group-format-wrapper {
clear: both;
padding: 0 0 0.6em;
}
(function ($, Drupal) {
'use strict';
Drupal.FieldGroup = Drupal.FieldGroup || {};
Drupal.FieldGroup.Effects = Drupal.FieldGroup.Effects || {};
/**
* This script transforms a set of fieldsets into a stack of horizontal
* tabs. Another tab pane can be selected by clicking on the respective
* tab.
*
* Each tab may have a summary which can be updated by another
* script. For that to work, each fieldset has an associated
* 'horizontalTabCallback' (with jQuery.data() attached to the fieldset),
* which is called every time the user performs an update to a form
* element inside the tab pane.
*/
Drupal.behaviors.horizontalTabs = {
attach: function (context) {
var width = drupalSettings.widthBreakpoint || 640;
var mq = '(max-width: ' + width + 'px)';
if (window.matchMedia(mq).matches) {
return;
}
$(context).find('[data-horizontal-tabs-panes]').once('horizontal-tabs').each(function () {
var $this = $(this).addClass('horizontal-tabs-panes');
var focusID = $(':hidden.horizontal-tabs-active-tab', this).val();
var tab_focus;
// Check if there are some details that can be converted to horizontal-tabs
var $details = $this.find('> details');
if ($details.length === 0) {
return;
}
// If collapse.js did not do his work yet, call it directly.
if (!$($details[0]).hasClass('.collapse-processed')) {
Drupal.behaviors.collapse.attach(context);
}
// Create the tab column.
var tab_list = $('<ul class="horizontal-tabs-list"></ul>');
$(this).wrap('<div class="horizontal-tabs clearfix"></div>').before(tab_list);
// Transform each details into a tab.
$details.each(function (i) {
var $this = $(this);
var summaryElement = $this.find('> summary .details-title');
if (!summaryElement.length) {
summaryElement = $this.find('> summary');
}
var summaryText = summaryElement.clone().children().remove().end().text().trim() || summaryElement.find('> span:first-child').text().trim();
var horizontal_tab = new Drupal.horizontalTab({
title: summaryText,
details: $this
});
horizontal_tab.item.addClass('horizontal-tab-button-' + i);
tab_list.append(horizontal_tab.item);
$this
.removeClass('collapsed')
// prop() can't be used on browsers not supporting details element,
// the style won't apply to them if prop() is used.
.attr('open', true)
.addClass('horizontal-tabs-pane')
.data('horizontalTab', horizontal_tab);
if (this.id === focusID) {
tab_focus = $this;
}
});
$(tab_list).find('> li:first').addClass('first');
$(tab_list).find('> li:last').addClass('last');
if (!tab_focus) {
// If the current URL has a fragment and one of the tabs contains an
// element that matches the URL fragment, activate that tab.
var hash = window.location.hash.replace(/[=%;,\/]/g, '');
if (hash !== '#' && $(this).find(hash).length) {
tab_focus = $(this).find(hash).closest('.horizontal-tabs-pane');
}
else {
tab_focus = $this.find('> .horizontal-tabs-pane:first');
}
}
if (tab_focus.length) {
tab_focus.data('horizontalTab').focus();
}
});
}
};
/**
* The horizontal tab object represents a single tab within a tab group.
*
* @param {object} settings
* An object with the following keys:
* - title: The name of the tab.
* - details: The jQuery object of the details element that is the tab pane.
*/
Drupal.horizontalTab = function (settings) {
var self = this;
$.extend(this, settings, Drupal.theme('horizontalTab', settings));
this.link.attr('href', '#' + settings.details.attr('id'));
this.link.on('click', function (e) {
e.preventDefault();
self.focus();
});
// Keyboard events added:
// Pressing the Enter key will open the tab pane.
this.link.on('keydown', function (event) {
if (event.keyCode === 13) {
event.preventDefault();
self.focus();
// Set focus on the first input field of the visible details/tab pane.
$('.horizontal-tabs-pane :input:visible:enabled:first').trigger('focus');
}
});
// Only bind update summary on forms.
if (this.details.drupalGetSummary) {
this.details
.on('summaryUpdated', function () {
self.updateSummary();
})
.trigger('summaryUpdated');
}
};
Drupal.horizontalTab.prototype = {
/**
* Displays the tab's content pane.
*/
focus: function () {
this.details
.removeClass('horizontal-tab-hidden')
.siblings('.horizontal-tabs-pane')
.each(function () {
var tab = $(this).data('horizontalTab');
tab.details.addClass('horizontal-tab-hidden');
tab.details.hide();
tab.item.removeClass('selected');
})
.end()
.show()
.siblings(':hidden.horizontal-tabs-active-tab')
.val(this.details.attr('id'));
this.item.addClass('selected');
// Mark the active tab for screen readers.
$('#active-horizontal-tab').remove();
this.link.append('<span id="active-horizontal-tab" class="visually-hidden">' + Drupal.t('(active tab)') + '</span>');
},
/**
* Updates the tab's summary.
*/
updateSummary: function () {
this.summary.html(this.details.drupalGetSummary());
},
/**
* Shows a horizontal tab pane.
*
* @return {Drupal.horizontalTab} The current horizontal tab.
*/
tabShow: function () {
// Display the tab.
this.item.removeClass('horizontal-tab-hidden');
this.item.show();
// Update .first marker for items. We need recurse from parent to retain the
// actual DOM element order as jQuery implements sortOrder, but not as public
// method.
this.item.parent().children('.horizontal-tab-button').removeClass('first')
.filter(':visible:first').addClass('first');
// Display the details element.
this.details.removeClass('horizontal-tab-hidden');
// Focus this tab.
this.focus();
return this;
},
/**
* Hides a horizontal tab pane.
*
* @return {Drupal.horizontalTab} The current horizontal tab.
*/
tabHide: function () {
// Hide this tab.
this.item.addClass('horizontal-tab-hidden');
this.item.hide();
// Update .first marker for items. We need recurse from parent to retain the
// actual DOM element order as jQuery implements sortOrder, but not as public
// method.
this.item.parent().children('.horizontal-tab-button').removeClass('first')
.filter(':visible:first').addClass('first');
// Hide the details element.
this.details.addClass('horizontal-tab-hidden');
// Focus the first visible tab (if there is one).
var $firstTab = this.details.siblings('.horizontal-tabs-pane:not(.horizontal-tab-hidden):first');
if ($firstTab.length) {
$firstTab.data('horizontalTab').focus();
}
else {
// Hide the vertical tabs (if no tabs remain).
this.item.closest('.form-type-horizontal-tabs').hide();
}
return this;
}
};
/**
* Theme function for a horizontal tab.
*
* @param {object} settings
* An object with the following keys:
* - title: The name of the tab.
* @return {object}
* This function has to return an object with at least these keys:
* - item: The root tab jQuery element
* - link: The anchor tag that acts as the clickable area of the tab
* (jQuery version)
* - summary: The jQuery element that contains the tab summary
*/
Drupal.theme.horizontalTab = function (settings) {
var tab = {};
var idAttr = settings.details.attr('id');
tab.item = $('<li class="horizontal-tab-button" tabindex="-1"></li>')
.append(tab.link = $('<a href="#' + idAttr + '"></a>')
.append(tab.title = $('<strong></strong>').text(settings.title))
);
// No need to add summary on frontend.
if (settings.details.drupalGetSummary) {
tab.link.append(tab.summary = $('<span class="summary"></span>'));
}
return tab;
};
})(jQuery, Drupal);
(function ($) {
'use strict';
Drupal.FieldGroup = Drupal.FieldGroup || {};
Drupal.FieldGroup.Effects = Drupal.FieldGroup.Effects || {};
/**
* Implements Drupal.FieldGroup.processHook().
*/
Drupal.FieldGroup.Effects.processTabs = {
execute: function (context, settings, group_info) {
if (group_info.context === 'form') {
// Add required fields mark to any element containing required fields
var direction = group_info.settings.direction;
$(context).find('[data-' + direction + '-tabs-panes]').each(function () {
var errorFocussed = false;
$(this).find('> details').once('fieldgroup-effects').each(function () {
var $this = $(this);
if (typeof $this.data(direction + 'Tab') !== 'undefined') {
if ($this.is('.required-fields') && ($this.find('[required]').length > 0 || $this.find('.form-required').length > 0)) {
$this.data(direction + 'Tab').link.find('strong:first').addClass('form-required');
}
if ($('.error', $this).length) {
$this.data(direction + 'Tab').link.parent().addClass('error');
// Focus the first tab with error.
if (!errorFocussed) {
Drupal.FieldGroup.setGroupWithfocus($this);
$this.data(direction + 'Tab').focus();
errorFocussed = true;
}
}
}
});
});
}
}
};
})(jQuery, Modernizr);
(function ($) {
'use strict';
Drupal.behaviors.fieldUIFieldsOverview = {
attach: function (context, settings) {
$('table#field-overview', context).once('field-field-overview', function () {
Drupal.fieldUIOverview.attach(this, settings.fieldUIRowsData, Drupal.fieldUIFieldOverview);
});
}
};
/**
* Row handlers for the 'Manage fields' screen.
*/
Drupal.fieldUIFieldOverview = Drupal.fieldUIFieldOverview || {};
Drupal.fieldUIFieldOverview.group = function (row, data) {
this.row = row;
this.name = data.name;
this.region = data.region;
this.tableDrag = data.tableDrag;
// Attach change listener to the 'group format' select.
this.$formatSelect = $('select.field-group-type', row);
this.$formatSelect.change(Drupal.fieldUIOverview.onChange);
return this;
};
Drupal.fieldUIFieldOverview.group.prototype = {
getRegion: function () {
return 'main';
},
regionChange: function (region, recurse) {
return {};
},
regionChangeFields: function (region, element, refreshRows) {
// Create a new tabledrag rowObject, that will compute the group's child
// rows for us.
var tableDrag = element.tableDrag;
var rowObject = new tableDrag.row(element.row, 'mouse', true);
// Skip the main row, we handled it above.
rowObject.group.shift();
// Let child rows handlers deal with the region change - without recursing
// on nested group rows, we are handling them all here.
$.each(rowObject.group, function () {
var childRow = this;
var childRowHandler = $(childRow).data('fieldUIRowHandler');
$.extend(refreshRows, childRowHandler.regionChange(region, false));
});
}
};
/**
* Row handlers for the 'Manage display' screen.
*/
Drupal.fieldUIDisplayOverview = Drupal.fieldUIDisplayOverview || {};
Drupal.fieldUIDisplayOverview.group = function (row, data) {
this.row = row;
this.name = data.name;
this.region = data.region;
this.tableDrag = data.tableDrag;
// Attach change listener to the 'group format' select.
this.$regionSelect = $(row).find('select.field-region');
this.$regionSelect.on('change', Drupal.fieldUIOverview.onChange);
return this;
};
Drupal.fieldUIDisplayOverview.group.prototype = {
getRegion: function getRegion() {
return this.$regionSelect.val();
},
regionChange: function (region, recurse) {
// Default recurse to true.
recurse = (typeof recurse === 'undefined') || recurse;
// When triggered by a row drag, the 'region' select needs to be adjusted to
// the new region.
region = region.replace(/-/g, '_');
this.$regionSelect.val(region);
var refreshRows = {};
refreshRows[this.name] = this.$regionSelect.get(0);
if (recurse) {
this.regionChangeFields(region, this, refreshRows);
}
return refreshRows;
},
regionChangeFields: function (region, element, refreshRows) {
// Create a new tabledrag rowObject, that will compute the group's child
// rows for us.
var tableDrag = element.tableDrag;
var rowObject = new tableDrag.row(element.row, 'mouse', true);
// Skip the main row, we handled it above.
rowObject.group.shift();
// Let child rows handlers deal with the region change - without recursing
// on nested group rows, we are handling them all here.
$.each(rowObject.group, function () {
var childRow = this;
var childRowHandler = $(childRow).data('fieldUIRowHandler');
$.extend(refreshRows, childRowHandler.regionChange(region, false));
});
}
};
})(jQuery);
(function ($) {
'use strict';
/**
* Drupal FieldGroup object.
*/
Drupal.FieldGroup = Drupal.FieldGroup || {};
Drupal.FieldGroup.Effects = Drupal.FieldGroup.Effects || {};
Drupal.FieldGroup.groupWithfocus = null;
Drupal.FieldGroup.setGroupWithfocus = function (element) {
element.css({display: 'block'});
Drupal.FieldGroup.groupWithfocus = element;
};
/**
* Behaviors.
*/
Drupal.behaviors.fieldGroup = {
attach: function (context, settings) {
settings.field_group = settings.field_group || drupalSettings.field_group;
if (typeof settings.field_group === 'undefined') {
return;
}
// Execute all of them.
$.each(Drupal.FieldGroup.Effects, function (func) {
// We check for a wrapper function in Drupal.field_group as
// alternative for dynamic string function calls.
var type = func.toLowerCase().replace('process', '');
if (typeof settings.field_group[type] !== 'undefined' && $.isFunction(this.execute)) {
this.execute(context, settings, settings.field_group[type]);
}
});
// Add a new ID to each fieldset.
$('.group-wrapper fieldset').each(function () {
// Tats bad, but we have to keep the actual id to prevent layouts to break.
var fieldgroupID = 'field_group-' + $(this).attr('id') + ' ' + $(this).attr('id');
$(this).attr('id', fieldgroupID);
});
// Set the hash in url to remember last userselection.
$('.group-wrapper ul li').each(function () {
var fieldGroupNavigationListIndex = $(this).index();
$(this).children('a').click(function () {
var fieldset = $('.group-wrapper fieldset').get(fieldGroupNavigationListIndex);
// Grab the first id, holding the wanted hashurl.
var hashUrl = $(fieldset).attr('id').replace(/^field_group-/, '').split(' ')[0];
window.location.hash = hashUrl;
});
});
}
};
})(jQuery);
<?php
namespace Drupal\field_group\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a FieldGroupFormatter annotation object.
*
* Formatters handle the display of fieldgroups.
*
* Additional annotation keys for formatters can be defined in
* hook_field_group_formatter_info_alter().
*
* @Annotation
*
* @see \Drupal\field_group\FieldGroupFormatterPluginManager
* @see \Drupal\field_group\FieldGroupFormatterInterface
*
* @ingroup field_formatter
*/
class FieldGroupFormatter extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the formatter type.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
/**
* The name of the fieldgroup formatter class.
*
* This is not provided manually, it will be added by the discovery mechanism.
*
* @var string
*/
public $class;
/**
* An array of contexts the formatter supports (form / view).
*
* @var array
*/
public $supported_contexts = [];
/**
* The different format types available for this formatter.
*
* @var array
*/
public $format_types = [];
/**
* An integer to determine the weight of this formatter relative to other
* formatter in the Field UI when selecting a formatter for a given group.
*
* @var int
*/
public $weight = NULL;
}
<?php
namespace Drupal\field_group\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\RenderElement;
/**
* Provides a render element for an accordion.
*
* @FormElement("field_group_accordion")
*/
class Accordion extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#process' => [
[$class, 'processGroup'],
[$class, 'processAccordion'],
],
'#pre_render' => [
[$class, 'preRenderGroup'],
],
'#theme_wrappers' => ['field_group_accordion'],
];
}
/**
* Process the accordion item.
*
* @param array $element
* An associative array containing the properties and children of the
* details element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* The processed element.
*/
public static function processAccordion(array &$element, FormStateInterface $form_state) {
// Add the jQuery UI accordion.
$element['#attached']['library'][] = 'field_group/formatter.accordion';
$element['#attached']['library'][] = 'field_group/core';
// Add the effect class.
if (isset($element['#effect'])) {
if (!isset($element['#attributes']['class'])) {
$element['#attributes']['class'] = [];
}
$element['#attributes']['class'][] = 'effect-' . $element['#effect'];
}
return $element;
}
}
<?php
namespace Drupal\field_group\Element;
use Drupal\Core\Render\Element\RenderElement;
/**
* Provides a render element for an accordion item.
*
* @FormElement("field_group_accordion_item")
*/
class AccordionItem extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#process' => [
[$class, 'processGroup'],
],
'#pre_render' => [
[$class, 'preRenderGroup'],
],
'#open' => FALSE,
'#theme_wrappers' => ['field_group_accordion_item'],
];
}
}
<?php
namespace Drupal\field_group\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\RenderElement;
/**
* Provides a render element for horizontal tabs.
*
* Formats all child details and all non-child details whose #group is
* assigned this element's name as horizontal tabs.
*
* @FormElement("horizontal_tabs")
*/
class HorizontalTabs extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#default_tab' => '',
'#process' => [
[$class, 'processHorizontalTabs'],
[$class, 'processGroup'],
],
'#pre_render' => [
[$class, 'preRenderGroup'],
],
'#theme_wrappers' => ['horizontal_tabs'],
];
}
/**
* Pre render the group to support #group parameter.
*
* @param array $element
* An associative array containing the properties and children of the
* element.
*
* @return array
* The modified element with all group members.
*/
public static function preRenderGroup($element) {
// The element may be rendered outside of a Form API context.
if (!isset($element['#parents']) || !isset($element['#groups'])) {
return $element;
}
if (isset($element['#group'])) {
// Contains form element summary functionalities.
$element['#attached']['library'][] = 'core/drupal.form';
$group = $element['#group'];
// If this element belongs to a group, but the group-holding element does
// not exist, we need to render it (at its original location).
if (!isset($element['#groups'][$group]['#group_exists'])) {
// Intentionally empty to clarify the flow; we simply return $element.
}
// If we injected this element into the group, then we want to render it.
elseif (!empty($element['#group_details'])) {
// Intentionally empty to clarify the flow; we simply return $element.
}
// Otherwise, this element belongs to a group and the group exists, so we do
// not render it.
elseif (Element::children($element['#groups'][$group])) {
$element['#printed'] = TRUE;
}
}
// Search for the correct default active tab.
$group_identifier = implode('][', $element['#parents']);
if (!empty($element['#groups'][$group_identifier])) {
$children = Element::children($element['#groups'][$group_identifier], TRUE);
foreach ($children as $key) {
if (!empty($element['#groups'][$group_identifier][$key]['#open'])) {
$element['#default_tab'] = $element['#groups'][$group_identifier][$key]['#id'];
$element[str_replace('][', '__', $group_identifier) . '__active_tab']['#value'] = $element['#default_tab'];
}
}
}
return $element;
}
/**
* Creates a group formatted as horizontal tabs.
*
* @param array $element
* An associative array containing the properties and children of the
* details element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param bool $on_form
* Are the tabs rendered on a form or not.
*
* @return array
* The processed element.
*/
public static function processHorizontalTabs(array &$element, FormStateInterface $form_state, $on_form = TRUE) {
// Inject a new details as child, so that form_process_details() processes
// this details element like any other details.
$element['group'] = [
'#type' => 'details',
'#theme_wrappers' => [],
'#parents' => $element['#parents'],
];
// Add an invisible label for accessibility.
if (!isset($element['#title'])) {
$element['#title'] = t('Horizontal Tabs');
$element['#title_display'] = 'invisible';
}
// Add required JavaScript and Stylesheet.
$element['#attached']['library'][] = 'field_group/element.horizontal_tabs';
// Only add forms library on forms.
if ($on_form) {
$element['#attached']['library'][] = 'core/drupal.form';
}
$name = implode('__', $element['#parents']);
if ($form_state->hasValue($name . '__active_tab')) {
$element['#default_tab'] = $form_state->getValue($name . '__active_tab');
}
$displayed_tab = isset($element['#default_tab']) ? $element['#default_tab'] : '';
// The JavaScript stores the currently selected tab in this hidden
// field so that the active tab can be restored the next time the
// form is rendered, e.g. on preview pages or when form validation
// fails.
$element['#default_tab'] = $displayed_tab;
$element[$name . '__active_tab'] = [
'#type' => 'hidden',
'#default_value' => $element['#default_tab'],
'#attributes' => ['class' => ['horizontal-tabs-active-tab']],
];
return $element;
}
/**
* Arranges elements into groups.
*
* This method is useful for non-input elements that can be used in and
* outside the context of a form.
*
* @param array $element
* An associative array containing the properties and children of the
* element. Note that $element must be taken by reference here, so processed
* child elements are taken over into $form_state.
* @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.
*/
public static function processGroup(&$element, FormStateInterface $form_state, &$complete_form) {
$groups = &$form_state->getGroups();
$element['#groups'] = &$groups;
if (isset($element['#group'])) {
// Add this element to the defined group (by reference).
$group = $element['#group'];
$groups[$group][] = &$element;
}
return $element;
}
}
<?php
namespace Drupal\field_group\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\RenderElement;
/**
* Provides a render element for a html element.
*
* @FormElement("field_group_html_element")
*/
class HtmlElement extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#process' => [
[$class, 'processGroup'],
[$class, 'processHtmlElement'],
],
'#pre_render' => [
[$class, 'preRenderGroup'],
],
'#theme_wrappers' => ['field_group_html_element'],
];
}
/**
* Process a html element.
*
* @param array $element
* An associative array containing the properties and children of the
* details element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* The processed element.
*/
public static function processHtmlElement(array &$element, FormStateInterface $form_state) {
// If an effect is set, we need to load extra js.
if (!empty($element['#effect']) && $element['#effect'] !== 'none') {
$element['#attached']['library'][] = 'field_group/formatter.html_element';
$element['#attached']['library'][] = 'field_group/core';
// Add the required classes for the js.
$element['#attributes']['class'][] = 'field-group-html-element';
$element['#attributes']['class'][] = 'fieldgroup-collapsible';
$element['#attributes']['class'][] = 'effect-' . $element['#effect'];
if (!empty($element['#speed'])) {
$element['#attributes']['class'][] = 'speed-' . $element['#speed'];
}
// Add jquery ui effects library for the blind effect.
if ($element['#effect'] == 'blind') {
$element['#attached']['library'][] = 'core/jquery.ui.effects.blind';
}
}
return $element;
}
}
<?php
namespace Drupal\field_group\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\RenderCallbackInterface;
/**
* Provides extra processing and pre rendering on the vertical tabs.
*/
class VerticalTabs implements RenderCallbackInterface {
/**
* Pre render the group to support #group parameter.
*
* @param array $element
* An associative array containing the properties and children of the
* element.
*
* @return array
* The modified element with all group members.
*/
public static function preRenderGroup($element) {
// The element may be rendered outside of a Form API context.
if (!isset($element['#parents']) || !isset($element['#groups'])) {
return $element;
}
if (isset($element['#group'])) {
// Contains form element summary functionalities.
$element['#attached']['library'][] = 'core/drupal.form';
$group = $element['#group'];
// If this element belongs to a group, but the group-holding element does
// not exist, we need to render it (at its original location).
if (!isset($element['#groups'][$group]['#group_exists'])) {
// Intentionally empty to clarify the flow; we simply return $element.
}
// If we injected this element into the group, then we want to render it.
elseif (!empty($element['#group_details'])) {
// Intentionally empty to clarify the flow; we simply return $element.
}
// Otherwise, this element belongs to a group and the group exists, so we do
// not render it.
elseif (Element::children($element['#groups'][$group])) {
$element['#printed'] = TRUE;
}
}
// Search for the correct default active tab.
$group_identifier = implode('][', $element['#parents']);
if (!empty($element['#groups'][$group_identifier])) {
$children = Element::children($element['#groups'][$group_identifier], TRUE);
foreach ($children as $key) {
if (!empty($element['#groups'][$group_identifier][$key]['#open'])) {
$element['#default_tab'] = $element['#groups'][$group_identifier][$key]['#id'];
$element[str_replace('][', '__', $group_identifier) . '__active_tab']['#value'] = $element['#default_tab'];
}
}
}
return $element;
}
/**
* Arranges elements into groups.
*
* This method is useful for non-input elements that can be used in and
* outside the context of a form.
*
* @param array $element
* An associative array containing the properties and children of the
* element. Note that $element must be taken by reference here, so processed
* child elements are taken over into $form_state.
* @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.
*/
public static function processGroup(&$element, FormStateInterface $form_state, &$complete_form) {
$groups = &$form_state->getGroups();
$element['#groups'] = &$groups;
if (isset($element['#group'])) {
// Add this element to the defined group (by reference).
$group = $element['#group'];
$groups[$group][] = &$element;
}
return $element;
}
}
<?php
namespace Drupal\field_group;
use Drupal\Core\Field\PluginSettingsBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Base class for 'Fieldgroup formatter' plugin implementations.
*
* @ingroup field_group_formatter
*/
abstract class FieldGroupFormatterBase extends PluginSettingsBase implements FieldGroupFormatterInterface {
/**
* The group this formatter needs to render.
*
* @var object
*/
protected $group;
/**
* The formatter settings.
*
* @var array
*/
protected $settings;
/**
* The label display setting.
*
* @var string
*/
protected $label;
/**
* The view mode.
*
* @var string
*/
protected $viewMode;
/**
* The context mode.
*
* @var string
*/
protected $context;
/**
* Constructs a FieldGroupFormatterBase object.
*
* @param string $plugin_id
* The plugin_id for the formatter.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \stdClass $group
* The group object.
* @param array $settings
* The formatter settings.
* @param string $label
* The formatter label.
*/
public function __construct($plugin_id, $plugin_definition, \stdClass $group, array $settings, $label) {
parent::__construct([], $plugin_id, $plugin_definition);
$this->group = $group;
$this->settings = $settings;
$this->label = $label;
$this->context = $group->context;
}
/**
* Get the current label.
*
* @return string
* The current label.
*/
public function getLabel() {
return $this->label;
}
/**
* {@inheritdoc}
*/
public function settingsForm() {
$class = get_class($this);
$form = [];
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Field group label'),
'#default_value' => $this->label,
'#weight' => -5,
];
$form['id'] = [
'#title' => $this->t('ID'),
'#type' => 'textfield',
'#default_value' => $this->getSetting('id'),
'#weight' => 10,
'#element_validate' => [[$class, 'validateId']],
];
$form['classes'] = [
'#title' => $this->t('Extra CSS classes'),
'#type' => 'textfield',
'#default_value' => $this->getSetting('classes'),
'#weight' => 11,
'#element_validate' => [[$class, 'validateCssClass']],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
if ($this->getSetting('formatter')) {
$summary[] = $this->pluginDefinition['label'] . ': ' . $this->getSetting('formatter');
}
if ($this->getSetting('id')) {
$summary[] = $this->t('Id: @id', ['@id' => $this->getSetting('id')]);
}
if ($this->getSetting('classes')) {
$summary[] = \Drupal::translation()->translate('Extra CSS classes: @classes', ['@classes' => $this->getSetting('classes')]);
}
return $summary;
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return self::defaultContextSettings('view');
}
/**
* {@inheritdoc}
*/
public static function defaultContextSettings($context) {
return [
'classes' => '',
'id' => '',
];
}
/**
* Get the classes to add to the group.
*/
protected function getClasses() {
$classes = [];
// Add a required-fields class to trigger the js.
if ($this->getSetting('required_fields')) {
$classes[] = 'required-fields';
$classes[] = 'field-group-' . str_replace('_', '-', $this->getBaseId());
}
if ($this->getSetting('classes')) {
$classes = array_merge($classes, explode(' ', trim($this->getSetting('classes'))));
}
return $classes;
}
/**
* {@inheritdoc}
*/
public function preRender(&$element, $rendering_object) {
$element['#group_name'] = $this->group->group_name;
$element['#entity_type'] = $this->group->entity_type;
$element['#bundle'] = $this->group->bundle;
}
/**
* {@inheritdoc}
*/
public function process(&$element, $processed_object) {
$element['#group_name'] = $this->group->group_name;
$element['#entity_type'] = $this->group->entity_type;
$element['#bundle'] = $this->group->bundle;
// BC: Call the pre render layer to not break contrib plugins.
return $this->preRender($element, $processed_object);
}
/**
* Validate the entered css class from the submitted format settings.
*
* @param array $element
* The validated element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the form.
*/
public static function validateCssClass(array $element, FormStateInterface $form_state) {
$form_state_values = $form_state->getValues();
$plugin_name = $form_state->get('plugin_settings_edit');
if (!empty($form_state_values['fields'][$plugin_name]['settings_edit_form']['settings']['classes']) && !preg_match('!^[A-Za-z0-9-_ ]+$!', $form_state_values['fields'][$plugin_name]['settings_edit_form']['settings']['classes'])) {
$form_state->setError($element, t('The css class must include only letters, numbers, underscores and dashes.'));
}
}
/**
* Validate the entered id attribute from the submitted format settings.
*
* @param array $element
* The validated element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the form.
*/
public static function validateId(array $element, FormStateInterface $form_state) {
$form_state_values = $form_state->getValues();
$plugin_name = $form_state->get('plugin_settings_edit');
if (!empty($form_state_values['fields'][$plugin_name]['settings_edit_form']['settings']['id']) && !preg_match('!^[A-Za-z0-9-_]+$!', $form_state_values['fields'][$plugin_name]['settings_edit_form']['settings']['id'])) {
$form_state->setError($element, t('The id must include only letters, numbers, underscores and dashes.'));
}
}
}
<?php
namespace Drupal\field_group;
use Drupal\Component\Plugin\PluginInspectionInterface;
/**
* Interface definition for fieldgroup formatter plugins.
*
* @ingroup field_group_formatter
*/
interface FieldGroupFormatterInterface extends PluginInspectionInterface {
/**
* Allows the field group formatter to manipulate the field group array and attach the formatters elements.
* The process method is called in the #process part of theme layer, and is currently used for forms.
* The preRender method is called in the #pre_render part of the theme layer, and is currently used for entity displays.
*
* @param array $element
* The field group render array.
* @param object $processed_object
* The object / entity beïng processed.
*/
public function process(&$element, $processed_object);
/**
* Allows the field group formatter to manipulate the field group array and attach the formatters rendering element.
*
* @param array $element
* The field group render array.
* @param object $rendering_object
* The object / entity beïng rendered.
*/
public function preRender(&$element, $rendering_object);
/**
* Returns a form to configure settings for the formatter.
*
* Invoked in field_group_field_ui_display_form_alter to allow
* administrators to configure the formatter. The field_group module takes care
* of handling submitted form values.
*
* @return array
* The form elements for the formatter settings.
*/
public function settingsForm();
/**
* Returns a short summary for the current formatter settings.
*
* If an empty result is returned, a UI can still be provided to display
* a settings form in case the formatter has configurable settings.
*
* @return array
* A short summary of the formatter settings.
*/
public function settingsSummary();
/**
* Defines the default settings for this plugin.
*
* @param string $context
* The context to get the default settings for.
*
* @return array
* A list of default settings, keyed by the setting name.
*/
public static function defaultContextSettings($context);
}
<?php
namespace Drupal\field_group;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* Plugin type manager for all fieldgroup formatters.
*/
class FieldGroupFormatterPluginManager extends DefaultPluginManager {
/**
* Constructs a new FieldGroupFormatterPluginManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/field_group/FieldGroupFormatter', $namespaces, $module_handler, 'Drupal\field_group\FieldGroupFormatterInterface', 'Drupal\field_group\Annotation\FieldGroupFormatter');
$this->alterInfo('field_group_formatter_info');
$this->setCacheBackend($cache_backend, 'field_group_formatter_info');
}
/**
* {@inheritdoc}
*/
public function createInstance($plugin_id, array $configuration = []) {
$plugin_definition = $this->getDefinition($plugin_id);
$plugin_class = DefaultFactory::getPluginClass($plugin_id, $plugin_definition);
// If the plugin provides a factory method, pass the container to it.
if (is_subclass_of($plugin_class, 'Drupal\Core\Plugin\ContainerFactoryPluginInterface')) {
return $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition);
}
return new $plugin_class($plugin_id, $plugin_definition, $configuration['group'], $configuration['settings'], $configuration['label']);
}
/**
* Overrides PluginManagerBase::getInstance().
*
* @param array $options
* An array with the following key/value pairs:
* - format_type: The current format type.
* - group: The current group.
* - prepare: (bool, optional) Whether default values should get merged in
* the 'configuration' array. Defaults to TRUE.
* - configuration: (array) the configuration for the formatter. The
* following key value pairs are allowed, and are all optional if
* 'prepare' is TRUE:
* - label: (string) Position of the label. The default 'field' theme
* implementation supports the values 'inline', 'above' and 'hidden'.
* Defaults to 'above'.
* - settings: (array) Settings specific to the formatter. Each setting
* defaults to the default value specified in the formatter definition.
*
* @return \Drupal\field_group\FieldGroupFormatterInterface|null
* A formatter object or NULL when plugin is not found.
*/
public function getInstance(array $options) {
$configuration = $options['configuration'];
$format_type = $options['format_type'];
$context = $options['group']->context;
// Fill in default configuration if needed.
if (!isset($options['prepare']) || $options['prepare'] == TRUE) {
$configuration = $this->prepareConfiguration($format_type, $context, $configuration);
}
$plugin_id = $format_type;
// Validate if plugin exists and it's allowed for current context.
$definition = $this->getDefinition($format_type, FALSE);
if (!isset($definition['class']) || !in_array($context, $definition['supported_contexts'])) {
return NULL;
}
$configuration += [
'group' => $options['group'],
];
return $this->createInstance($plugin_id, $configuration);
}
/**
* Merges default values for formatter configuration.
*
* @param string $format_type
* The format type.
* @param string $context
* The context to prepare configuration for.
* @param array $configuration
* The configuration of the group.
* @return array
* The display properties with defaults added.
*/
public function prepareConfiguration($format_type, $context, array $configuration) {
// Fill in defaults for missing properties.
$configuration += [
'label' => '',
'settings' => [],
];
// Fill in default settings values for the formatter.
$configuration['settings'] += $this->getDefaultSettings($format_type, $context);
return $configuration;
}
/**
* Returns the default settings of a field_group formatter.
*
* @param string $type
* A formatter type name.
* @param string $context
* The context to get default values for.
*
* @return array
* The formatter type's default settings, as provided by the plugin
* definition, or an empty array if type or settings are undefined.
*/
public function getDefaultSettings($type, $context) {
$plugin_definition = $this->getDefinition($type, FALSE);
if (!empty($plugin_definition['class'])) {
$plugin_class = DefaultFactory::getPluginClass($type, $plugin_definition);
return $plugin_class::defaultContextSettings($context);
}
return [];
}
}
<?php
namespace Drupal\field_group;
use Drupal\Core\Url;
use Drupal\field_ui\FieldUI;
/**
* Static methods for fieldgroup UI.
*/
class FieldgroupUi {
/**
* Get the field ui route that should be used for given arguments.
*
* @param object $group
* The group to get the field ui route for.
*
* @return \Drupal\Core\Url
* A URL object.
*/
public static function getFieldUiRoute($group) {
$entity_type = \Drupal::entityTypeManager()->getDefinition($group->entity_type);
if ($entity_type->get('field_ui_base_route')) {
$context_route_name = "";
$mode_route_name = "default";
$route_parameters = FieldUI::getRouteBundleParameter($entity_type, $group->bundle);
// Get correct route name based on context and mode.
if ($group->context == 'form') {
$context_route_name = 'entity_form_display';
if ($group->mode != 'default') {
$mode_route_name = 'form_mode';
$route_parameters['form_mode_name'] = $group->mode;
}
}
else {
$context_route_name = 'entity_view_display';
if ($group->mode != 'default') {
$mode_route_name = 'view_mode';
$route_parameters['view_mode_name'] = $group->mode;
}
}
return new Url("entity.{$context_route_name}.{$group->entity_type}.{$mode_route_name}", $route_parameters);
}
}
/**
* Get the field group delete route for a given group.
*
* @param \stdClass $group
* The group to delete.
*
* @return \Drupal\Core\Url
* A URL object.
*/
public static function getDeleteRoute(\stdClass $group) {
$entity_type_id = $group->entity_type;
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
if ($entity_type->get('field_ui_base_route')) {
$mode_route_name = '';
$route_parameters = FieldUI::getRouteBundleParameter($entity_type, $group->bundle);
$route_parameters['field_group_name'] = $group->group_name;
// Get correct route name based on context and mode.
if ($group->context == 'form') {
$context_route_name = 'form_display';
if ($group->mode != 'default') {
$mode_route_name = '.form_mode';
$route_parameters['form_mode_name'] = $group->mode;
}
}
else {
$context_route_name = 'display';
if ($group->mode != 'default') {
$mode_route_name = '.view_mode';
$route_parameters['view_mode_name'] = $group->mode;
}
}
return new Url('field_ui.field_group_delete_' . $entity_type_id . '.' . $context_route_name . $mode_route_name, $route_parameters);
}
throw new \InvalidArgumentException('The given group is not a valid.');
}
}
<?php
namespace Drupal\field_group\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field_group\FieldGroupFormatterPluginManager;
use Drupal\field_group\FieldgroupUi;
use Drupal\field_group\FormatterHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for adding a fieldgroup to a bundle.
*/
class FieldGroupAddForm extends FormBase {
/**
* The prefix for groups.
*
* @var string
*/
const GROUP_PREFIX = 'group_';
/**
* The name of the entity type.
*
* @var string
*/
protected $entityTypeId;
/**
* The entity bundle.
*
* @var string
*/
protected $bundle;
/**
* The context for the group.
*
* @var string
*/
protected $context;
/**
* The mode for the group.
*
* @var string
*/
protected $mode;
/**
* Current step of the form.
*
* @var string
*/
protected $currentStep;
/**
* The field group formatter plugin manager.
*
* @var \Drupal\field_group\FieldGroupFormatterPluginManager
*/
protected $fieldGroupFormatterPluginManager;
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* FieldGroupAddForm constructor.
*
* @param \Drupal\field_group\FieldGroupFormatterPluginManager $fieldGroupFormatterPluginManager
* The field group formatter plugin manager.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
*/
public function __construct(FieldGroupFormatterPluginManager $fieldGroupFormatterPluginManager, MessengerInterface $messenger) {
$this->fieldGroupFormatterPluginManager = $fieldGroupFormatterPluginManager;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.field_group.formatters'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'field_group_add_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL, $bundle = NULL, $context = NULL) {
$this->entityTypeId = $entity_type_id;
$this->bundle = $bundle;
$this->context = $context;
if ($context == 'form') {
$this->mode = $this->getRequest()->get('form_mode_name');
}
else {
$this->mode = $this->getRequest()->get('view_mode_name');
}
if (empty($this->mode)) {
$this->mode = 'default';
}
if (!$form_state->get('step')) {
$form_state->set('step', 'formatter');
}
$this->currentStep = $form_state->get('step');
if ($this->currentStep == 'formatter') {
$this->buildFormatterSelectionForm($form, $form_state);
}
else {
$this->buildConfigurationForm($form, $form_state);
}
return $form;
}
/**
* Build the formatter selection step.
*/
public function buildFormatterSelectionForm(array &$form, FormStateInterface $form_state) {
// Gather group formatters.
$formatter_options = FormatterHelper::formatterOptions($this->context);
$form['add'] = [
'#type' => 'container',
'#attributes' => ['class' => ['form--inline', 'clearfix']],
];
$form['add']['group_formatter'] = [
'#type' => 'select',
'#title' => $this->t('Add a new group'),
'#options' => $formatter_options,
'#empty_option' => $this->t('- Select a field group type -'),
'#required' => TRUE,
];
// Field label and field_name.
$form['new_group_wrapper'] = [
'#type' => 'container',
'#states' => [
'!visible' => [
':input[name="group_formatter"]' => ['value' => ''],
],
],
];
$form['new_group_wrapper']['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#size' => 15,
'#required' => TRUE,
];
$form['new_group_wrapper']['group_name'] = [
'#type' => 'machine_name',
'#size' => 15,
// This field should stay LTR even for RTL languages.
'#field_prefix' => '<span dir="ltr">' . self::GROUP_PREFIX,
'#field_suffix' => '</span>&lrm;',
'#description' => $this->t('A unique machine-readable name containing letters, numbers, and underscores.'),
'#maxlength' => FieldStorageConfig::NAME_MAX_LENGTH - strlen(self::GROUP_PREFIX),
'#machine_name' => [
'source' => ['new_group_wrapper', 'label'],
'exists' => [$this, 'groupNameExists'],
],
'#required' => TRUE,
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save and continue'),
'#button_type' => 'primary',
'#validate' => [
[$this, 'validateFormatterSelection'],
],
];
$form['#attached']['library'][] = 'field_ui/drupal.field_ui';
}
/**
* Build the formatter configuration form.
*/
public function buildConfigurationForm(array &$form, FormStateInterface $form_state) {
$group = new \stdClass();
$group->context = $this->context;
$group->entity_type = $this->entityTypeId;
$group->bundle = $this->bundle;
$group->mode = $this->mode;
$manager = $this->fieldGroupFormatterPluginManager;
$plugin = $manager->getInstance([
'format_type' => $form_state->getValue('group_formatter'),
'configuration' => [
'label' => $form_state->getValue('label'),
],
'group' => $group,
]);
$form['format_settings'] = [
'#type' => 'container',
'#tree' => TRUE,
];
$form['format_settings'] += $plugin->settingsForm();
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Create group'),
'#button_type' => 'primary',
];
}
/**
* Validate the formatter selection step.
*/
public function validateFormatterSelection(array &$form, FormStateInterface $form_state) {
$group_name = self::GROUP_PREFIX . $form_state->getValue('group_name');
// Add the prefix.
$form_state->setValueForElement($form['new_group_wrapper']['group_name'], $group_name);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->get('step') == 'formatter') {
$form_state->set('step', 'configuration');
$form_state->set('group_label', $form_state->getValue('label'));
$form_state->set('group_name', $form_state->getValue('group_name'));
$form_state->set('group_formatter', $form_state->getValue('group_formatter'));
$form_state->setRebuild();
}
else {
$new_group = (object) [
'group_name' => $form_state->get('group_name'),
'entity_type' => $this->entityTypeId,
'bundle' => $this->bundle,
'mode' => $this->mode,
'context' => $this->context,
'children' => [],
'parent_name' => '',
'weight' => 20,
'format_type' => $form_state->get('group_formatter'),
'region' => 'hidden',
];
$new_group->format_settings = $form_state->getValue('format_settings');
$new_group->label = $new_group->format_settings['label'];
unset($new_group->format_settings['label']);
$new_group->format_settings += $this->fieldGroupFormatterPluginManager->getDefaultSettings($form_state->get('group_formatter'), $this->context);
field_group_group_save($new_group);
// Store new group information for any additional submit handlers.
$groups_added = $form_state->get('groups_added');
$groups_added['_add_new_group'] = $new_group->group_name;
$this->messenger->addMessage($this->t('New group %label successfully created.', ['%label' => $new_group->label]));
$form_state->setRedirectUrl(FieldgroupUi::getFieldUiRoute($new_group));
\Drupal::cache()->invalidate('field_groups');
}
}
/**
* Checks if a group machine name is taken.
*
* @param string $value
* The machine name, not prefixed.
* @param array $element
* An array containing the structure of the 'group_name' element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return bool
* Whether or not the group machine name is taken.
*/
public function groupNameExists($value, array $element, FormStateInterface $form_state) {
// Add the prefix.
$group_name = self::GROUP_PREFIX . $value;
return field_group_exists($group_name, $this->entityTypeId, $this->bundle, $this->context, $this->mode);
}
}
<?php
namespace Drupal\field_group\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\field_group\FieldgroupUi;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for removing a fieldgroup from a bundle.
*/
class FieldGroupDeleteForm extends ConfirmFormBase {
/**
* The fieldgroup to delete.
*
* @var object
*/
protected $fieldGroup;
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* FieldGroupDeleteForm constructor.
*
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
*/
public function __construct(MessengerInterface $messenger) {
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'field_group_delete_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $field_group_name = NULL, $entity_type_id = NULL, $bundle = NULL, $context = NULL) {
if ($context == 'form') {
$mode = $this->getRequest()->attributes->get('form_mode_name');
}
else {
$mode = $this->getRequest()->attributes->get('view_mode_name');
}
if (empty($mode)) {
$mode = 'default';
}
$this->fieldGroup = field_group_load_field_group($field_group_name, $entity_type_id, $bundle, $context, $mode);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$bundles = \Drupal::service('entity_type.bundle.info')->getAllBundleInfo();
$bundle_label = $bundles[$this->fieldGroup->entity_type][$this->fieldGroup->bundle]['label'];
field_group_delete_field_group($this->fieldGroup);
$this->messenger->addMessage($this->t('The group %group has been deleted from the %type content type.', ['%group' => $this->fieldGroup->label, '%type' => $bundle_label]));
// Redirect.
$form_state->setRedirectUrl($this->getCancelUrl());
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete the group %group?', ['%group' => $this->fieldGroup->label]);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return FieldgroupUi::getFieldUiRoute($this->fieldGroup);
}
}
<?php
namespace Drupal\field_group;
use Drupal;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
/**
* Static methods for fieldgroup formatters.
*/
class FormatterHelper implements TrustedCallbackInterface {
/**
* Return an array of field_group_formatter options.
*/
public static function formatterOptions($type) {
$options = &drupal_static(__FUNCTION__);
if (!isset($options)) {
$options = [];
$manager = \Drupal::service('plugin.manager.field_group.formatters');
$formatters = $manager->getDefinitions();
foreach ($formatters as $formatter) {
if (in_array($type, $formatter['supported_contexts'])) {
$options[$formatter['id']] = $formatter['label'];
}
}
}
return $options;
}
/**
* Pre render callback for rendering groups on entities without theme hook.
*
* @param array $element
* Entity being rendered.
*
* @return array
*/
public static function entityViewPrender(array $element) {
field_group_build_entity_groups($element, 'view');
return $element;
}
/**
* Process callback for field groups.
*
* @param array $element
* Form that is being processed.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $form
* The complete form structure.
*
* @return array
*/
public static function formProcess(array &$element, FormStateInterface $form_state = NULL, array &$form = []) {
if (empty($element['#field_group_form_process'])) {
$element['#field_group_form_process'] = TRUE;
if (empty($element['#fieldgroups'])) {
return $element;
}
// Create all groups and keep a flat list of references to these groups.
$group_references = [];
foreach ($element['#fieldgroups'] as $group_name => $group) {
if (!isset($element[$group_name])) {
$element[$group_name] = [];
}
$group_parents = $element['#array_parents'];
$group_parents[] = empty($group->parent_name) ? $group->region : $group->parent_name;
$group_references[$group_name] = &$element[$group_name];
$element[$group_name]['#group'] = implode('][', $group_parents);
// Use array parents to set the group name. This will cover multilevel forms (eg paragraphs).
$parents = $element['#array_parents'];
$parents[] = $group_name;
$element[$group_name]['#parents'] = $parents;
$group_children_parent_group = implode('][', $parents);
foreach ($group->children as $child) {
if (!empty($element[$child]['#field_group_ignore'])) {
continue;
}
$element[$child]['#group'] = $group_children_parent_group;
}
}
foreach ($element['#fieldgroups'] as $group_name => $group) {
$field_group_element = &$element[$group_name];
// Let modules define their wrapping element.
// Note that the group element has no properties, only elements.
foreach (Drupal::moduleHandler()->getImplementations('field_group_form_process') 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_form_process';
$function($field_group_element, $group, $element);
}
// Allow others to alter the pre_render.
Drupal::moduleHandler()->alter('field_group_form_process', $field_group_element, $group, $element);
}
// Allow others to alter the complete processed build.
Drupal::moduleHandler()->alter('field_group_form_process_build', $element, $form_state, $form);
}
return $element;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['entityViewPrender', 'formProcess'];
}
}
<?php
namespace Drupal\field_group\Plugin\Derivative;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Component\Plugin\Derivative\DeriverBase;
/**
* Provides local action definitions for all entity bundles.
*/
class FieldGroupLocalAction extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a FieldUiLocalAction object.
*
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider to load routes by name.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(RouteProviderInterface $route_provider, EntityTypeManagerInterface $entity_type_manager) {
$this->routeProvider = $route_provider;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('router.route_provider'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->get('field_ui_base_route')) {
$default_options = [
'title' => $this->t('Add field group'),
];
$this->derivatives['field_group_add_' . $entity_type_id . '_form_display'] = [
'route_name' => "field_ui.field_group_add_$entity_type_id.form_display",
'appears_on' => [
"entity.entity_form_display.{$entity_type_id}.default",
],
] + $default_options;
$this->derivatives['field_group_add_' . $entity_type_id . '_form_display_form_mode'] = [
'route_name' => "field_ui.field_group_add_$entity_type_id.form_display.form_mode",
'appears_on' => [
"entity.entity_form_display.{$entity_type_id}.form_mode",
],
] + $default_options;
$this->derivatives['field_group_add_' . $entity_type_id . '_display'] = [
'route_name' => "field_ui.field_group_add_$entity_type_id.display",
'appears_on' => [
"entity.entity_view_display.{$entity_type_id}.default",
],
] + $default_options;
$this->derivatives['field_group_add_' . $entity_type_id . '_display_view_mode'] = [
'route_name' => "field_ui.field_group_add_$entity_type_id.display.view_mode",
'appears_on' => [
"entity.entity_view_display.{$entity_type_id}.view_mode",
],
] + $default_options;
}
}
foreach ($this->derivatives as &$entry) {
$entry += $base_plugin_definition;
}
return $this->derivatives;
}
}
<?php
namespace Drupal\field_group\Plugin\field_group\FieldGroupFormatter;
use Drupal\field_group\Element\Accordion as AccordionElement;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormState;
use Drupal\field_group\FieldGroupFormatterBase;
/**
* Plugin implementation of the 'accordion' formatter.
*
* @FieldGroupFormatter(
* id = "accordion",
* label = @Translation("Accordion"),
* description = @Translation("This fieldgroup renders child groups as jQuery accordion."),
* supported_contexts = {
* "form",
* "view",
* }
* )
*/
class Accordion extends FieldGroupFormatterBase {
/**
* {@inheritdoc}
*/
public function process(&$element, $processed_object) {
// Keep using preRender parent for BC.
parent::preRender($element, $processed_object);
$element += [
'#type' => 'field_group_accordion',
'#effect' => $this->getSetting('effect'),
];
if ($this->getSetting('id')) {
$element['#id'] = Html::getUniqueId($this->getSetting('id'));
}
$classes = $this->getClasses();
if (!empty($classes)) {
$element += ['#attributes' => ['class' => $classes]];
}
}
/**
* {@inheritdoc}
*/
public function preRender(&$element, $rendering_object) {
$this->process($element, $rendering_object);
$form_state = new FormState();
AccordionElement::processAccordion($element, $form_state);
}
/**
* {@inheritdoc}
*/
public function settingsForm() {
$form = parent::settingsForm();
$form['effect'] = [
'#title' => $this->t('Effect'),
'#type' => 'select',
'#options' => ['none' => $this->t('None'), 'bounceslide' => $this->t('Bounce slide')],
'#default_value' => $this->getSetting('effect'),
'#weight' => 2,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$summary[] = $this->t('Effect : @effect',
['@effect' => $this->getSetting('effect')]
);
return $summary;
}
/**
* {@inheritdoc}
*/
public static function defaultContextSettings($context) {
return [
'effect' => 'none',
] + parent::defaultSettings($context);
}
}
<?php
namespace Drupal\field_group\Plugin\field_group\FieldGroupFormatter;
use Drupal\Component\Utility\Html;
use Drupal\field_group\FieldGroupFormatterBase;
/**
* Plugin implementation of the 'accordion_item' formatter.
*
* @FieldGroupFormatter(
* id = "accordion_item",
* label = @Translation("Accordion Item"),
* description = @Translation("This fieldgroup renders the content in a div, part of accordion group."),
* format_types = {
* "open",
* "closed",
* },
* supported_contexts = {
* "form",
* "view",
* },
* )
*/
class AccordionItem extends FieldGroupFormatterBase {
/**
* {@inheritdoc}
*/
public function process(&$element, $processed_object) {
// Keep using preRender parent for BC.
parent::preRender($element, $processed_object);
$element += [
'#type' => 'field_group_accordion_item',
'#effect' => $this->getSetting('effect'),
'#title' => $this->getLabel(),
];
if ($this->getSetting('id')) {
$element['#id'] = Html::getUniqueId($this->getSetting('id'));
}
$classes = $this->getClasses();
if (!empty($classes)) {
$element += ['#attributes' => ['class' => $classes]];
}
if ($this->getSetting('required_fields')) {
$element['#attached']['library'][] = 'field_group/formatter.details';
}
if ($this->getSetting('formatter') == 'open') {
$element['#open'] = TRUE;
}
foreach ($element as $key => $value) {
if (is_array($value) && !empty($value['#children_errors'])) {
$element['#open'] = TRUE;
}
}
}
/**
* {@inheritdoc}
*/
public function preRender(&$element, $rendering_object) {
parent::preRender($element, $rendering_object);
$this->process($element, $rendering_object);
}
/**
* {@inheritdoc}
*/
public function settingsForm() {
$form = parent::settingsForm();
$form['formatter'] = [
'#title' => $this->t('Default state'),
'#type' => 'select',
'#options' => array_combine($this->pluginDefinition['format_types'], $this->pluginDefinition['format_types']),
'#default_value' => $this->getSetting('formatter'),
'#weight' => -4,
];
if ($this->context == 'form') {
$form['required_fields'] = [
'#type' => 'checkbox',
'#title' => $this->t('Mark group as required if it contains required fields.'),
'#default_value' => $this->getSetting('required_fields'),
'#weight' => 2,
];
}
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
if ($this->getSetting('required_fields')) {
$summary[] = $this->t('Mark as required');
}
if ($this->getSetting('description')) {
$summary[] = $this->t('Description : @description',
['@description' => $this->getSetting('description')]
);
}
return $summary;
}
/**
* {@inheritdoc}
*/
public static function defaultContextSettings($context) {
$defaults = [
'formatter' => 'closed',
'description' => '',
] + parent::defaultSettings($context);
if ($context == 'form') {
$defaults['required_fields'] = 1;
}
return $defaults;
}
}
<?php
namespace Drupal\field_group\Plugin\field_group\FieldGroupFormatter;
use Drupal\Component\Utility\Html;
use Drupal\field_group\FieldGroupFormatterBase;
/**
* Details element.
*
* @FieldGroupFormatter(
* id = "details",
* label = @Translation("Details"),
* description = @Translation("Add a details element"),
* supported_contexts = {
* "form",
* "view"
* }
* )
*/
class Details extends FieldGroupFormatterBase {
/**
* {@inheritdoc}
*/
public function process(&$element, $processed_object) {
$element += [
'#type' => 'details',
'#title' => $this->getLabel(),
'#open' => $this->getSetting('open'),
'#description' => $this->getSetting('description'),
];
if ($this->getSetting('id')) {
$element['#id'] = Html::getUniqueId($this->getSetting('id'));
}
$classes = $this->getClasses();
if (!empty($classes)) {
$element += [
'#attributes' => ['class' => $classes],
];
}
if ($this->getSetting('required_fields')) {
$element['#attached']['library'][] = 'field_group/formatter.details';
$element['#attached']['library'][] = 'field_group/core';
}
}
/**
* {@inheritdoc}
*/
public function preRender(&$element, $rendering_object) {
parent::preRender($element, $rendering_object);
$this->process($element, $rendering_object);
}
/**
* {@inheritdoc}
*/
public function settingsForm() {
$form = parent::settingsForm();
$form['description'] = [
'#title' => $this->t('Description'),
'#type' => 'textarea',
'#default_value' => $this->getSetting('description'),
'#weight' => -4,
];
$form['open'] = [
'#type' => 'checkbox',
'#title' => $this->t('Display element open by default.'),
'#default_value' => $this->getSetting('open'),
];
if ($this->context == 'form') {
$form['required_fields'] = [
'#type' => 'checkbox',
'#title' => $this->t('Mark group as required if it contains required fields.'),
'#default_value' => $this->getSetting('required_fields'),
'#weight' => 2,
];
}
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
if ($this->getSetting('open')) {
$summary[] = $this->t('Default state open');
}
else {
$summary[] = $this->t('Default state closed');
}
if ($this->getSetting('required_fields')) {
$summary[] = $this->t('Mark as required');
}
return $summary;
}
/**
* {@inheritdoc}
*/
public static function defaultContextSettings($context) {
$defaults = [
'open' => FALSE,
'required_fields' => $context == 'form',
] + parent::defaultSettings($context);
if ($context == 'form') {
$defaults['required_fields'] = 1;
}
return $defaults;
}
}
<?php
namespace Drupal\field_group\Plugin\field_group\FieldGroupFormatter;
/**
* Details Sidebar element.
*
* @FieldGroupFormatter(
* id = "details_sidebar",
* label = @Translation("Details Sidebar"),
* description = @Translation("Add a details sidebar element"),
* supported_contexts = {
* "form",
* "view"
* }
* )
*/
class DetailsSidebar extends Details {
/**
* {@inheritdoc}
*/
public function process(&$element, $processed_object) {
parent::process($element, $processed_object);
$element['#group'] = 'advanced';
if ($this->getSetting('weight')) {
$element['#weight'] = $this->getSetting('weight');
}
}
/**
* {@inheritdoc}
*/
public function settingsForm() {
$form = parent::settingsForm();
$form['weight'] = [
'#type' => 'number',
'#title' => $this->t('Weight'),
'#default_value' => $this->getSetting('weight'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
if ($this->getSetting('weight')) {
$summary[] = $this->t('Weight: @weight',
['@weight' => $this->getSetting('weight')]
);
}
return $summary;
}
/**
* {@inheritdoc}
*/
public static function defaultContextSettings($context) {
$defaults = parent::defaultContextSettings($context);
$defaults['weight'] = 0;
return $defaults;
}
}
<?php
namespace Drupal\field_group\Plugin\field_group\FieldGroupFormatter;
use Drupal\Component\Utility\Html;
use Drupal\field_group\FieldGroupFormatterBase;
/**
* Plugin implementation of the 'fieldset' formatter.
*
* @FieldGroupFormatter(
* id = "fieldset",
* label = @Translation("Fieldset"),
* description = @Translation("This fieldgroup renders the inner content in a fieldset with the title as legend."),
* supported_contexts = {
* "form",
* "view",
* }
* )
*/
class Fieldset extends FieldGroupFormatterBase {
/**
* {@inheritdoc}
*/
public function process(&$element, $processed_object) {
$element += [
'#type' => 'fieldset',
'#title' => $this->getLabel(),
'#attributes' => [],
'#description' => $this->getSetting('description'),
];
// When a fieldset has a description, an id is required.
if ($this->getSetting('description') && !$this->getSetting('id')) {
$element['#id'] = Html::getUniqueId($this->group->group_name);
}
if ($this->getSetting('id')) {
$element['#id'] = Html::getUniqueId($this->getSetting('id'));
}
$classes = $this->getClasses();
if (!empty($classes)) {
$element['#attributes'] += ['class' => $classes];
}
if ($this->getSetting('required_fields')) {
$element['#attached']['library'][] = 'field_group/formatter.fieldset';
$element['#attached']['library'][] = 'field_group/core';
}
}
/**
* {@inheritdoc}
*/
public function preRender(&$element, $rendering_object) {
parent::preRender($element, $rendering_object);
$this->process($element, $rendering_object);
}
/**
* {@inheritdoc}
*/
public function settingsForm() {
$form = parent::settingsForm();
$form['description'] = [
'#title' => $this->t('Description'),
'#type' => 'textarea',
'#default_value' => $this->getSetting('description'),
'#weight' => -4,
];
if ($this->context == 'form') {
$form['required_fields'] = [
'#type' => 'checkbox',
'#title' => $this->t('Mark group as required if it contains required fields.'),
'#default_value' => $this->getSetting('required_fields'),
'#weight' => 2,
];
}
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
if ($this->getSetting('required_fields')) {
$summary[] = $this->t('Mark as required');
}
return $summary;
}
/**
* {@inheritdoc}
*/
public static function defaultContextSettings($context) {
$defaults = [
'description' => '',
] + parent::defaultSettings($context);
if ($context == 'form') {
$defaults['required_fields'] = 1;
}
return $defaults;
}
}
<?php
namespace Drupal\field_group\Plugin\field_group\FieldGroupFormatter;
use Drupal\field_group\Element\HtmlElement as HtmlElementRenderElement;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormState;
use Drupal\Core\Template\Attribute;
use Drupal\field_group\FieldGroupFormatterBase;
/**
* Plugin implementation of the 'html_element' formatter.
*
* @FieldGroupFormatter(
* id = "html_element",
* label = @Translation("HTML element"),
* description = @Translation("This fieldgroup renders the inner content in a HTML element with classes and attributes."),
* supported_contexts = {
* "form",
* "view",
* }
* )
*/
class HtmlElement extends FieldGroupFormatterBase {
/**
* {@inheritdoc}
*/
public function process(&$element, $processed_object) {
// Keep using preRender parent for BC.
parent::preRender($element, $processed_object);
$element_attributes = new Attribute();
if ($this->getSetting('attributes')) {
// This regex split the attributes string so that we can pass that
// later to drupal_attributes().
preg_match_all('/([^\s=]+)="([^"]+)"/', $this->getSetting('attributes'), $matches);
// Put the attribute and the value together.
foreach ($matches[1] as $key => $attribute) {
$element_attributes[$attribute] = $matches[2][$key];
}
}
// Add the id to the attributes array.
if ($this->getSetting('id')) {
$element_attributes['id'] = Html::getUniqueId($this->getSetting('id'));
}
// Add the classes to the attributes array.
$classes = $this->getClasses();
if (!empty($classes)) {
if (!isset($element_attributes['class'])) {
$element_attributes['class'] = [];
}
// If user also entered class in the attributes textfield, force it to an array.
else {
$element_attributes['class'] = [$element_attributes['class']];
}
$element_attributes['class'] = array_merge($classes, $element_attributes['class']->value());
}
$element['#effect'] = $this->getSetting('effect');
$element['#speed'] = $this->getSetting('speed');
$element['#type'] = 'field_group_html_element';
$element['#wrapper_element'] = $this->getSetting('element');
$element['#attributes'] = $element_attributes;
if ($this->getSetting('show_label')) {
$element['#title_element'] = $this->getSetting('label_element');
$element['#title'] = $this->getLabel();
$element['#title_attributes'] = new Attribute();
if (!empty($this->getSetting('label_element_classes'))) {
$element['#title_attributes']->addClass(explode(' ', $this->getSetting('label_element_classes')));
}
if (!empty($this->getSetting('effect')) && $this->getSetting('effect') !== 'none') {
$element['#title_attributes']->addClass('field-group-toggler');
}
}
if ($this->getSetting('required_fields')) {
$element['#attributes']['class'][] = 'field-group-html-element';
$element['#attached']['library'][] = 'field_group/formatter.html_element';
$element['#attached']['library'][] = 'field_group/core';
}
}
/**
* {@inheritdoc}
*/
public function preRender(&$element, $rendering_object) {
$this->process($element, $rendering_object);
$form_state = new FormState();
HtmlElementRenderElement::processHtmlElement($element, $form_state);
}
/**
* {@inheritdoc}
*/
public function settingsForm() {
$form = parent::settingsForm();
$form['element'] = [
'#title' => $this->t('Element'),
'#type' => 'textfield',
'#default_value' => $this->getSetting('element'),
'#description' => $this->t('E.g. div, section, aside etc.'),
'#weight' => 1,
];
$form['show_label'] = [
'#title' => $this->t('Show label'),
'#type' => 'select',
'#options' => [0 => $this->t('No'), 1 => $this->t('Yes')],
'#default_value' => $this->getSetting('show_label'),
'#weight' => 2,
'#attributes' => [
'data-fieldgroup-selector' => 'show_label',
],
];
$form['label_element'] = [
'#title' => $this->t('Label element'),
'#type' => 'textfield',
'#default_value' => $this->getSetting('label_element'),
'#weight' => 3,
'#states' => [
'visible' => [
':input[data-fieldgroup-selector="show_label"]' => ['value' => 1],
],
],
];
$form['label_element_classes'] = [
'#title' => $this->t('Label element HTML classes'),
'#type' => 'textfield',
'#default_value' => $this->getSetting('label_element_classes'),
'#weight' => 3,
'#states' => [
'visible' => [
':input[data-fieldgroup-selector="show_label"]' => ['value' => 1],
],
],
];
if ($this->context == 'form') {
$form['required_fields'] = [
'#title' => $this->t('Mark group as required if it contains required fields.'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('required_fields'),
'#weight' => 4,
];
}
$form['attributes'] = [
'#title' => $this->t('Attributes'),
'#type' => 'textfield',
'#default_value' => $this->getSetting('attributes'),
'#description' => $this->t('E.g. name="anchor"'),
'#weight' => 5,
];
$form['effect'] = [
'#title' => $this->t('Effect'),
'#type' => 'select',
'#options' => [
'none' => $this->t('None'),
'collapsible' => $this->t('Collapsible'),
'blind' => $this->t('Blind'),
],
'#default_value' => $this->getSetting('effect'),
'#weight' => 6,
'#attributes' => [
'data-fieldgroup-selector' => 'effect',
],
];
$form['speed'] = [
'#title' => $this->t('Speed'),
'#type' => 'select',
'#options' => ['slow' => $this->t('Slow'), 'fast' => $this->t('Fast')],
'#default_value' => $this->getSetting('speed'),
'#weight' => 7,
'#states' => [
'!visible' => [
':input[data-fieldgroup-selector="effect"]' => ['value' => 'none'],
],
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
$summary[] = $this->t('Element: @element',
['@element' => $this->getSetting('element')]
);
if ($this->getSetting('show_label')) {
$summary[] = $this->t('Label element: @element',
['@element' => $this->getSetting('label_element')]
);
if (!empty($this->getSetting('label_element_classes'))) {
$summary[] = $this->t('Label element HTML classes: @label_classes', [
'@label_classes' => $this->getSetting('label_element_classes'),
]);
}
}
if ($this->getSetting('attributes')) {
$summary[] = $this->t('Attributes: @attributes',
['@attributes' => $this->getSetting('attributes')]
);
}
if ($this->getSetting('required_fields')) {
$summary[] = $this->t('Mark as required');
}
return $summary;
}
/**
* {@inheritdoc}
*/
public static function defaultContextSettings($context) {
$defaults = [
'element' => 'div',
'show_label' => 0,
'label_element' => 'h3',
'label_element_classes' => '',
'effect' => 'none',
'speed' => 'fast',
'attributes' => '',
] + parent::defaultSettings($context);
if ($context == 'form') {
$defaults['required_fields'] = 1;
}
return $defaults;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment