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.
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
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
* Allows administrators to attach field groups.
*/
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\ds\Ds;
use Drupal\Core\Entity\ContentEntityFormInterface;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\ConfirmFormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\field_group\Element\VerticalTabs;
use Drupal\field_group\FormatterHelper;
/**
* Implements hook_help().
*/
function field_group_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.field_group':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('Fieldgroup will, as the name implies, group fields together. All fieldable entities will have the possibility to add groups to wrap their fields together. Fieldgroup comes with default HTML wrappers like vertical tabs, horizontal tabs, accordions, fieldsets or div wrappers.') . '</p>';
$output .= '<p>' . t('The field group project is a follow-up on the field group module in <a href="@linkcck">CCK</a>. The release will only exist for Drupal 7 release and higher, so since the existence of the Fields API in core.</br>By moving field group to a separate module, this may open some new perspectives.', ['@linkcck' => 'http://drupal.org/project/cck']) . '</p>';
$output .= '<h3>' . t('More Information') . '</h3>';
$output .= '<p>' . t('For more information about this module please visit the <a href="@link">module page</a>.', ['@link' => 'https://www.drupal.org/project/field_group']) . '</p>';
return $output;
}
}
/**
* Implements hook_library_info_alter().
*/
function field_group_library_info_alter(&$libraries, $extension) {
// Swap jQuery.ui library if available.
// See https://www.drupal.org/project/field_group/issues/3109552 for more
// background on the logic.
if (version_compare(\Drupal::VERSION, 9) > 0 && $extension == 'field_group') {
if (\Drupal::moduleHandler()->moduleExists('jquery_ui_accordion')) {
$libraries['formatter.accordion']['dependencies'] = ['jquery_ui_accordion/accordion'];
}
else {
$libraries['formatter.accordion']['js'] = [];
$libraries['formatter.accordion']['dependencies'] = [];
}
}
}
/**
* Implements hook_theme_registry_alter().
*/
function field_group_theme_registry_alter(&$theme_registry) {
// Inject field_group_build_entity_groups in all entity theming functions.
$entity_info = Drupal::entityTypeManager()->getDefinitions();
$entity_types = [];
foreach ($entity_info as $entity_type_id => $entity_type) {
if ($route_name = $entity_type->get('field_ui_base_route')) {
$entity_types[] = $entity_type_id;
}
}
foreach ($theme_registry as $theme_hook => $info) {
if (in_array($theme_hook, $entity_types) || (!empty($info['base hook']) && in_array($info['base hook'], $entity_types))) {
$theme_registry[$theme_hook]['preprocess functions'][] = 'field_group_build_entity_groups';
}
}
// ECK does not use the eck as theme function.
if (isset($theme_registry['eck_entity'])) {
$theme_registry['eck_entity']['preprocess functions'][] = 'field_group_build_entity_groups';
}
}
/**
* Implements hook_theme().
*/
function field_group_theme() {
return [
'horizontal_tabs' => [
'render element' => 'element',
'template' => 'horizontal-tabs',
'file' => 'templates/theme.inc',
],
'field_group_accordion_item' => [
'render element' => 'element',
'template' => 'field-group-accordion-item',
'file' => 'templates/theme.inc',
],
'field_group_accordion' => [
'render element' => 'element',
'template' => 'field-group-accordion',
'file' => 'templates/theme.inc',
],
'field_group_html_element' => [
'render element' => 'element',
'template' => 'field-group-html-element',
'file' => 'templates/theme.inc',
],
];
}
/**
* Implements hook_theme_suggestions_alter().
*/
function field_group_theme_suggestions_alter(array &$suggestions, array $variables, $hook) {
switch ($hook) {
case 'horizontal_tabs':
case 'field_group_accordion_item':
case 'field_group_accordion':
case 'field_group_html_element':
$element = $variables['element'];
$name = !empty($element['#group_name']) ? $element['#group_name'] : NULL;
$entity_type = !empty($element['#entity_type']) ? $element['#entity_type'] : NULL;
$bundle = !empty($element['#bundle']) ? $element['#bundle'] : NULL;
$wrapper = '';
if (isset($element['#wrapper_element'])) {
$wrapper = $element['#wrapper_element'];
$suggestions[] = $hook . '__' . $wrapper;
}
if (!empty($entity_type)) {
$suggestions[] = $hook . '__' . $entity_type;
}
if (!empty($bundle)) {
$suggestions[] = $hook . '__' . $bundle;
}
if (!empty($name)) {
$suggestions[] = $hook . '__' . $name;
}
if ($wrapper && !empty($entity_type)) {
$suggestions[] = $hook . '__' . $entity_type . '__' . $wrapper;
}
if (!empty($entity_type) && !empty($bundle)) {
$suggestions[] = $hook . '__' . $entity_type . '__' . $bundle;
}
if (!empty($entity_type) && !empty($name)) {
$suggestions[] = $hook . '__' . $entity_type . '__' . $name;
}
if ($wrapper && !empty($entity_type) && !empty($bundle)) {
$suggestions[] = $hook . '__' . $entity_type . '__' . $bundle . '__' . $wrapper;
}
if (!empty($entity_type) && !empty($bundle) && !empty($name)) {
$suggestions[] = $hook . '__' . $entity_type . '__' . $bundle . '__' . $name;
}
break;
}
}
/**
* Implements hook_element_info_alter().
*/
function field_group_element_info_alter(array &$info) {
// Core does not support #group options on vertical tabs. Add support for it.
if (isset($info['vertical_tabs'])) {
if (!isset($info['vertical_tabs']['#process'])) {
$info['vertical_tabs']['#process'] = [];
}
if (!isset($info['vertical_tabs']['#pre_render'])) {
$info['vertical_tabs']['#pre_render'] = [];
}
$info['vertical_tabs']['#process'][] = [VerticalTabs::class, 'processGroup'];
$info['vertical_tabs']['#pre_render'][] = [VerticalTabs::class, 'preRenderGroup'];
}
}
/**
* Implements hook_form_FORM_ID_alter().
* Using hook_form_field_ui_form_display_overview_form_alter.
*/
function field_group_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
$form_state->loadInclude('field_group', 'inc', 'includes/field_ui');
field_group_field_ui_display_form_alter($form, $form_state);
}
/**
* Implements hook_form_FORM_ID_alter().
* Using hook_form_field_ui_display_overview_form_alter.
*/
function field_group_form_entity_view_display_edit_form_alter(&$form, FormStateInterface $form_state) {
$form_state->loadInclude('field_group', 'inc', 'includes/field_ui');
field_group_field_ui_display_form_alter($form, $form_state);
}
/**
* Implements hook_field_info_max_weight().
*/
function field_group_field_info_max_weight($entity_type, $bundle, $context, $context_mode) {
// Prevent recursion.
// @see https://www.drupal.org/project/drupal/issues/2966137
static $recursion_tracker = [];
// Track the entity display.
$key = $entity_type . ':' . $bundle . ':' . $context . ':' . $context_mode;
// If entity display check was attempted but did not finish, do not continue.
if (isset($recursion_tracker[$key])) {
return NULL;
}
// Mark this as an attempt at entity display check.
$recursion_tracker[$key] = TRUE;
$groups = field_group_info_groups($entity_type, $bundle, $context, $context_mode);
// Remove the indicator once the entity display is successfully checked.
unset($recursion_tracker[$key]);
$weights = [];
foreach ($groups as $group) {
$weights[] = $group->weight;
}
return $weights ? max($weights) : NULL;
}
/**
* Implements hook_form_alter().
*/
function field_group_form_alter(array &$form, FormStateInterface $form_state) {
$form_object = $form_state->getFormObject();
if ($form_object instanceof ContentEntityFormInterface && !$form_object instanceof ConfirmFormInterface) {
/**
* @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display
*/
$storage = $form_state->getStorage();
if (!empty($storage['form_display'])) {
$form_display = $storage['form_display'];
$entity = $form_object->getEntity();
$context = [
'entity_type' => $entity->getEntityTypeId(),
'bundle' => $entity->bundle(),
'entity' => $entity,
'context' => 'form',
'display_context' => 'form',
'mode' => $form_display->getMode(),
];
field_group_attach_groups($form, $context);
$form['#process'][] = [FormatterHelper::class, 'formProcess'];
}
}
}
/**
* Implements hook_inline_entity_form_entity_form_alter().
*/
function field_group_inline_entity_form_entity_form_alter(&$entity_form, FormStateInterface $form_state) {
// Attach the fieldgroups to current entity form.
$context = [
'entity_type' => $entity_form['#entity']->getEntityTypeId(),
'bundle' => $entity_form['#entity']->bundle(),
'entity' => $entity_form['#entity'],
'display_context' => 'form',
'mode' => isset($entity_form['#form_mode']) ? $entity_form['#form_mode'] : 'default',
];
field_group_attach_groups($entity_form, $context);
FormatterHelper::formProcess($entity_form, $form_state);
}
/**
* Implements hook_form_layout_builder_update_block_alter().
*/
function field_group_form_layout_builder_update_block_alter(&$form, FormStateInterface $form_state) {
// Attach fieldgroups to the layout builder form for custom block types.
if (!isset($form['settings']['block_form']['#block'])) {
return;
}
$context = [
'entity_type' => $form['settings']['block_form']['#block']->getEntityTypeId(),
'bundle' => $form['settings']['block_form']['#block']->bundle(),
'entity' => $form['settings']['block_form']['#block'],
'display_context' => 'form',
'mode' => 'default',
];
field_group_attach_groups($form['settings']['block_form'], $context);
$form['settings']['block_form']['#process'][] = [FormatterHelper::class, 'formProcess'];
}
/**
* Implements hook_form_layout_builder_add_block_alter().
*/
function field_group_form_layout_builder_add_block_alter(&$form, FormStateInterface $form_state) {
// Call the update hook.
field_group_form_layout_builder_update_block_alter($form, $form_state);
}
/**
* Implements hook_entity_view_alter().
*/
function field_group_entity_view_alter(&$build, EntityInterface $entity, EntityDisplayInterface $display) {
$context = [
'entity_type' => $display->getTargetEntityTypeId(),
'bundle' => $entity->bundle(),
'entity' => $entity,
'display_context' => 'view',
'mode' => $display->getMode(),
];
field_group_attach_groups($build, $context);
// If no theme hook, we have no theme hook to preprocess.
// Add a prerender.
if (empty($build['#theme'])) {
$ds_enabled = FALSE;
if (Drupal::moduleHandler()->moduleExists('ds')) {
// Check if DS is enabled for this display.
if ($display->getThirdPartySetting('ds', 'layout') && !Ds::isDisabled()) {
$ds_enabled = TRUE;
}
}
// If DS is enabled, no pre render is needed (DS adds fieldgroup preprocessing).
if (!$ds_enabled) {
$build['#pre_render'][] = [FormatterHelper::class, 'entityViewPrender'];
}
}
}
/**
* Pre render callback for rendering groups.
*
* @param array $element
* Form that is being rendered.
*
* @deprecated Use field_group_form_process instead.
*
* @return array
*/
function field_group_form_pre_render(array $element) {
return field_group_form_process($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
*/
function field_group_form_process(array &$element, FormStateInterface $form_state = NULL, array &$form = []) {
return FormatterHelper::formProcess($element, $form_state, $form);
}
/**
* Implements hook_field_group_form_process().
*/
function field_group_field_group_form_process(array &$element, &$group, &$complete_form) {
// 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,
]);
if ($plugin) {
$plugin->process($element, $complete_form);
}
}
/**
* Implements hook_field_group_pre_render().
*
* @param array $element
* Group beïng rendered.
* @param object $group
* The Field group info.
* @param $rendering_object
* The entity / form beïng rendered
*/
function field_group_field_group_pre_render(&$element, &$group, &$rendering_object) {
// Add all field_group format types to the js settings.
$element['#attached']['drupalSettings']['field_group'] = [
$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,
]);
if ($plugin) {
$plugin->preRender($element, $rendering_object);
}
}
/**
* Implements hook_field_group_form_process_build_alter().
*
* @param array $element
* Element being processed.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $form
* The complete form structure.
*/
function field_group_field_group_form_process_build_alter(array &$element, $form_state = NULL, &$form = []) {
$groups = array_keys($element['#fieldgroups']);
field_group_remove_empty_form_groups($element, $groups, $element['#entity_type']);
}
/**
* Implements hook_field_group_build_pre_render_alter().
*
* @param array $element
*/
function field_group_field_group_build_pre_render_alter(&$element) {
// Someone is doing a node view, in a node view. Reset content.
if (isset($element['#node']->content) && count($element['#node']->content) > 0) {
$element['#node']->content = [];
}
$display = isset($element['#view_mode']);
$groups = array_keys($element['#fieldgroups']);
// Dish the fieldgroups with no fields for non-forms.
if ($display) {
field_group_remove_empty_display_groups($element, $groups);
}
// Remove all empty groups on the form.
else {
field_group_remove_empty_form_groups($element, $groups, $element['#entity_type']);
}
}
/**
* Attach groups to the (form) build.
*
* @param array $element
* The part of the form.
* @param array $context
* The contextual information.
*/
function field_group_attach_groups(&$element, $context) {
if ($context['mode'] == '_custom') {
return;
}
$entity_type = $context['entity_type'];
$bundle = $context['bundle'];
$mode = $context['mode'];
$display_context = $context['display_context'];
$element['#fieldgroups'] = field_group_info_groups($entity_type, $bundle, $display_context, $mode);
// Create a lookup array.
$group_children = [];
foreach ($element['#fieldgroups'] as $group_name => $group) {
foreach ($group->children as $child) {
$group_children[$child] = $group_name;
}
}
$element['#group_children'] = $group_children;
$element['#entity_type'] = $entity_type;
}
/**
* Pre-render callback for entity views.
*
* @param preprocess $vars
* Variables.
* @param $context
* The display context (entity type, form or view).
*
* @return array
* With re-arranged fields in groups.
*
* @see field_group_theme_registry_alter
* @see field_group_fields_nest()
*/
function field_group_build_entity_groups(array &$vars, $context = 'view') {
if (isset($vars['elements'])) {
$element = &$vars['elements'];
}
elseif (isset($vars['content'])) {
$element = &$vars['content'];
}
else {
if ($context === 'eck_entity') {
$element = &$vars['entity'];
}
else {
$element = &$vars;
}
}
$nest_vars = &$vars;
// No groups on the entity.
if (empty($element['#fieldgroups'])) {
return $element;
}
// Use other nest function if field layout is active.
if (isset($element['_field_layout'])) {
field_group_field_layout_fields_nest($element, $nest_vars, $context);
}
else {
field_group_fields_nest($element, $nest_vars, $context);
}
// Allow others to alter the pre_rendered build.
Drupal::moduleHandler()->alter('field_group_build_pre_render', $element);
// No groups on the entity. Prerender removed empty field groups.
if (empty($element['#fieldgroups'])) {
return $element;
}
// Put groups inside content if we are rendering an entity_view.
$render_key = field_group_get_content_element_key($context);
foreach ($element['#fieldgroups'] as $group) {
if (!empty($element[$group->group_name])) {
if (isset($vars[$render_key])) {
// Field layout enabled? Place it in correct region of the
// _field_layout key.
if (isset($vars[$render_key]['_field_layout'])) {
$vars[$render_key]['_field_layout'][$group->region][$group->group_name] = $element[$group->group_name];
}
else {
$vars[$render_key][$group->group_name] = $element[$group->group_name];
}
}
}
}
}
/**
* Nests all the fields in the field groups.
*
* This function will take out all the elements in the form and
* place them in the correct container element, a fieldgroup.
* The current group element in the loop is passed recursively so we can
* stash fields and groups in it while we go deeper in the array.
*
* @param array $element
* The current element to analyse for grouping.
* @param array $vars
* Rendering vars from the entity being viewed.
* @param array $context
* The display context (entity type, form or view).
*/
function field_group_fields_nest(&$element, &$vars = NULL, $context = NULL) {
// Create all groups and keep a flat list of references to these groups.
$group_references = [];
foreach ($element['#fieldgroups'] as $group_name => $group) {
// Construct own weight, as some fields (for example preprocess fields) don't have weight set.
if (!isset($element[$group_name])) {
$element[$group_name] = [];
}
$group_references[$group_name] = &$element[$group_name];
}
// Loop through all form children looking for those that are supposed to be
// in groups, and insert placeholder element for the new group field in the
// correct location within the form structure.
$element_clone = [];
foreach (Element::children($element) as $child_name) {
$element_clone[$child_name] = $element[$child_name];
// If this element is in a group, create the placeholder element.
if (isset($element['#group_children'][$child_name])) {
$element_clone[$element['#group_children'][$child_name]] = [];
}
}
$element = array_merge($element_clone, $element);
// Move all children to their parents. Use the flat list of references for
// direct access as we don't know where in the root_element hierarchy the
// parent currently is situated.
foreach ($element['#group_children'] as $child_name => $parent_name) {
// Entity being viewed.
if ($vars) {
// If not a group, check the content variable for empty field.
$key = field_group_get_content_element_key($context);
if (!isset($element['#fieldgroups'][$child_name]) && isset($vars[$key][$child_name])) {
// ECK marks his defaut properties as printed, while it is not printed yet.
if ($context === 'eck_entity' && !empty($vars[$key][$child_name]['#printed'])) {
$vars[$key][$child_name]['#printed'] = FALSE;
}
$group_references[$parent_name][$child_name] = $vars[$key][$child_name];
unset($vars[$key][$child_name]);
}
// If this is a group, we have to use a reference to keep the reference
// list intact (but if it is a field we don't mind).
else {
$group_references[$parent_name][$child_name] = &$element[$child_name];
unset($element[$child_name]);
}
}
// Form being viewed.
else {
// Block denied fields (#access) before they are put in groups.
// Fields (not groups) that don't have children (like field_permissions) are removed
// in field_group_field_group_build_pre_render_alter.
if (isset($element[$child_name]) && (!isset($element[$child_name]['#access']) || $element[$child_name]['#access'])) {
// If this is a group, we have to use a reference to keep the reference
// list intact (but if it is a field we don't mind).
$group_references[$parent_name][$child_name] = &$element[$child_name];
// Remove the #group property, otherwise core will move this element to
// the field layout region.
unset($group_references[$parent_name][$child_name]['#group']);
$group_references[$parent_name]['#weight'] = $element['#fieldgroups'][$parent_name]->weight;
}
// The child has been copied to its parent: remove it from the root element.
unset($element[$child_name]);
}
}
// Bring extra element wrappers to achieve a grouping of fields.
// This will mainly be prefix and suffix altering.
foreach ($element['#fieldgroups'] as $group_name => $group) {
field_group_pre_render($group_references[$group_name], $group, $element);
}
}
/**
* Nests all the fields in the field groups.
*
* Ror entity display elements managed by field layout.
*
* @param array $element
* @param $vars
* @param $context
*/
function field_group_field_layout_fields_nest(array &$element, &$vars = NULL, $context = NULL) {
// Create all groups and keep a flat list of references to these groups.
$group_references = [];
foreach ($element['#fieldgroups'] as $group_name => $group) {
// Construct own weight, as some fields (for example preprocess fields)
// don't have weight set.
if (!isset($element[$group_name])) {
$element[$group_name] = [];
}
$group_references[$group_name] = &$element[$group_name];
}
// Loop through all children looking for those that are supposed to be
// in groups, and insert placeholder element for the new group field in the
// correct location within the form structure.
$element_clone = [];
foreach (Element::children($element['_field_layout']) as $region_name) {
foreach (Element::children($element['_field_layout'][$region_name]) as $child_name) {
$element_clone['_field_layout'][$region_name][$child_name] = $element['_field_layout'][$region_name][$child_name];
// If this element is in a group, create the placeholder element.
if (isset($element['_field_layout'][$region_name]['#group_children'][$child_name])) {
$element_clone['_field_layout'][$region_name][$element['#group_children'][$child_name]] = [];
}
}
}
$element = array_merge($element_clone, $element);
// Move all children to their parents. Use the flat list of references for
// direct access as we don't know where in the root_element hierarchy the
// parent currently is situated.
foreach ($element['#group_children'] as $child_name => $group_name) {
$region = $element['#fieldgroups'][$group_name]->region;
// If not a group, check the content variable for empty field.
$key = field_group_get_content_element_key($context);
if (!isset($element['#fieldgroups'][$child_name]) && isset($vars[$key]['_field_layout'][$region][$child_name])) {
// ECK marks his defaut properties as printed, while it is not printed yet.
if ($context === 'eck_entity' && !empty($vars[$key]['_field_layout'][$region][$child_name]['#printed'])) {
$vars[$key]['_field_layout'][$region][$child_name]['#printed'] = FALSE;
}
$group_references[$group_name][$child_name] = $vars[$key]['_field_layout'][$region][$child_name];
unset($vars[$key]['_field_layout'][$region][$child_name]);
}
// If this is a group, we have to use a reference to keep the reference
// list intact (but if it is a field we don't mind).
else {
$group_references[$group_name][$child_name] = &$element[$child_name];
unset($element[$child_name]);
}
}
// Bring extra element wrappers to achieve a grouping of fields.
// This will mainly be prefix and suffix altering.
foreach ($element['#fieldgroups'] as $group_name => $group) {
field_group_pre_render($group_references[$group_name], $group, $element);
}
}
/**
* Function to pre render the field group element.
*
* @see field_group_fields_nest()
*
* @param $element
* Render array of group element that needs to be created.
* @param $group
* Object with the group information.
* @param $rendering_object
* The entity / form beïng rendered.
*/
function field_group_pre_render(& $element, $group, & $rendering_object) {
// Only run the pre_render function if the group has elements.
// $group->group_name.
if ($element == []) {
return;
}
// Let modules define their wrapping element.
// Note that the group element has no properties, only elements.
foreach (Drupal::moduleHandler()->getImplementations('field_group_pre_render') as $module) {
// The intention here is to have the opportunity to alter the
// elements, as defined in hook_field_group_formatter_info.
// Note, implement $element by reference!
$function = $module . '_field_group_pre_render';
$function($element, $group, $rendering_object);
}
// Allow others to alter the pre_render.
Drupal::moduleHandler()->alter('field_group_pre_render', $element, $group, $rendering_object);
}
/**
* Provides the content element key for a display context.
*
* This allows entity modules to specify their content element for field group
* support, or other modules to add entity module support.
*
* @param $context
* The display context (entity type, form or view).
*
* @return string
*/
function field_group_get_content_element_key($context = 'default') {
$keys = &drupal_static('field_group_content_elements');
if (!isset($keys)) {
$keys['default'] = 'content';
// Allow other modules to alter the array.
Drupal::moduleHandler()->alter('field_group_content_element_keys', $keys);
}
// Check if we have a specific content element key for this entity type.
$key = $keys['default'];
if (isset($keys[$context])) {
$key = $keys[$context];
}
return $key;
}
/**
* Saves a group definition.
*
* @param \stdClass $group
* A group definition.
* @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display
* The display to update if known.
*
* @return \Drupal\Core\Entity\Display\EntityDisplayInterface|null
* The updated entity display.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
function field_group_group_save($group, $display = NULL) {
if ($display === NULL) {
if ($group->context == 'form') {
$display = EntityFormDisplay::load($group->entity_type . '.' . $group->bundle . '.' . $group->mode);
}
elseif ($group->context == 'view') {
$display = EntityViewDisplay::load($group->entity_type . '.' . $group->bundle . '.' . $group->mode);
}
}
// If no display was found. It doesn't exist yet, create it.
if (!isset($display)) {
if ($group->context == 'form') {
$display = EntityFormDisplay::create([
'targetEntityType' => $group->entity_type,
'bundle' => $group->bundle,
'mode' => $group->mode,
])->setStatus(TRUE);
}
elseif ($group->context == 'view') {
$display = EntityViewDisplay::create([
'targetEntityType' => $group->entity_type,
'bundle' => $group->bundle,
'mode' => $group->mode,
])->setStatus(TRUE);
}
}
if (isset($display)) {
// Remove label from the format_settings.
unset($group->format_settings['label']);
$data = (array) $group;
unset($data['group_name'], $data['entity_type'], $data['bundle'], $data['mode'], $data['form'], $data['context']);
$display->setThirdPartySetting('field_group', $group->group_name, $data);
$display->save();
}
return $display;
}
/**
* Delete a field group.
*
* @param $group
* A group definition.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
function field_group_delete_field_group($group) {
if ($group->context == 'form') {
$display = EntityFormDisplay::load($group->entity_type . '.' . $group->bundle . '.' . $group->mode);
}
elseif ($group->context == 'view') {
$display = EntityViewDisplay::load($group->entity_type . '.' . $group->bundle . '.' . $group->mode);
}
/**
* @var $display \Drupal\Core\Entity\Display\EntityDisplayInterface
*/
if (isset($display)) {
$display->unsetThirdPartySetting('field_group', $group->group_name);
$display->save();
}
Drupal::moduleHandler()->invokeAll('field_group_delete_field_group', [$group]);
}
/**
* Get all groups.
*
* @param $entity_type
* The name of the entity.
* @param $bundle
* The name of the bundle.
* @param $context
* The context of the view mode (form or view)
* @param $mode
* The view mode.
*
* @return array
*/
function field_group_info_groups($entity_type, $bundle, $context, $mode) {
if ($context == 'form') {
$display = EntityFormDisplay::load($entity_type . '.' . $bundle . '.' . $mode);
if (!$display) {
return [];
}
$data = $display->getThirdPartySettings('field_group');
}
if ($context == 'view') {
$display = EntityViewDisplay::load($entity_type . '.' . $bundle . '.' . $mode);
if (!$display) {
return [];
}
$data = $display->getThirdPartySettings('field_group');
}
$groups = [];
if (isset($data) && is_array($data)) {
foreach ($data as $group_name => $definition) {
$definition += [
'group_name' => $group_name,
'entity_type' => $entity_type,
'bundle' => $bundle,
'context' => $context,
'mode' => $mode,
];
$groups[$group_name] = (object) $definition;
}
}
return $groups;
}
/**
* Loads a group definition.
*
* @param $group_name
* The name of the group.
* @param $entity_type
* The name of the entity.
* @param $bundle
* The name of the bundle.
* @param $context
* The context of the view mode (form or view)
* @param $mode
* The view mode to load.
*
* @return mixed
*/
function field_group_load_field_group($group_name, $entity_type, $bundle, $context, $mode) {
$groups = field_group_info_groups($entity_type, $bundle, $context, $mode);
if (isset($groups[$group_name])) {
return $groups[$group_name];
}
}
/**
* Checks if a field_group exists in required context.
*
* @param string $group_name
* The name of the group.
* @param string $entity_type
* The name of the entity.
* @param string $bundle
* The bundle for the entity.
* @param $context
* The context of the view mode (form or view)
* @param string $mode
* The view mode context the group will be rendered.
*
* @return bool
*/
function field_group_exists($group_name, $entity_type, $bundle, $context, $mode) {
return (bool) field_group_load_field_group($group_name, $entity_type, $bundle, $context, $mode);
}
/**
* Remove empty groups on forms.
*
* @param array $element
* The element to check the empty state.
* @param array $groups
* Array of group objects.
* @param string $entity_type
* The entity type.
*/
function field_group_remove_empty_form_groups(&$element, $groups, $entity_type) {
$exceptions = ['user__account', 'comment__author'];
$children = Element::getVisibleChildren($element);
$empty_groups_indication = array_fill_keys($groups, TRUE);
if (count($children)) {
foreach ($children as $childname) {
$exception = $entity_type . '__' . $childname;
$empty_element = !(isset($element[$childname]['#type']) || isset($element[$childname]['#markup']) || in_array($exception, $exceptions));
// If the element is not empty, and it has a group. Mark the group as not
// empty.
if (!$empty_element && isset($element[$childname]['#group']) && (!isset($element[$childname]['#access']) || $element[$childname]['#access'])) {
$name_prefix = implode('][', $element['#array_parents']) . '][';
$group_name = str_replace($name_prefix, '', $element[$childname]['#group']);
$empty_groups_indication[$group_name] = FALSE;
}
}
}
// Set access to false for all empty groups.
$empty_groups = array_filter($empty_groups_indication);
foreach (array_keys($empty_groups) as $group_name) {
$element[$group_name]['#access'] = FALSE;
}
}
/**
* Remove empty groups on entity display.
*
* @param array $element
* The element to check the empty state.
* @param array $groups
* Array of group objects.
*
* @return bool
*/
function field_group_remove_empty_display_groups(& $element, $groups) {
$empty_child = TRUE;
$empty_group = TRUE;
// Loop through the visible children for current element.
foreach (Element::getVisibleChildren($element) as $name) {
// Descend if the child is a group.
if (in_array($name, $groups)) {
$empty_child = field_group_remove_empty_display_groups($element[$name], $groups);
if (!$empty_child) {
$empty_group = FALSE;
}
}
// Child is a field or a renderable array and the element is not empty.
elseif (!empty($element[$name])) {
$clone_element = $element[$name];
// Weight parameter can make empty element seen as not empty.
unset($clone_element['#weight']);
if (!Element::isEmpty($clone_element)) {
$empty_group = FALSE;
}
}
}
// Reset an empty group.
if ($empty_group) {
$element = [];
}
return $empty_group;
}
<?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);
<?php
/**
* @file
* Field_group.field_ui.inc is a file that contains most functions
* needed on the Fields UI Manage forms (display and fields).
*/
use Drupal\field_group\FieldgroupUi;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Entity\EntityDisplayBase;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\Element;
use Drupal\field_group\FormatterHelper;
use Drupal\field_ui\Form\EntityDisplayFormBase;
/**
* Helper function to get the form parameters to use while
* building the fields and display overview form.
*
* @param $form
*
* @param \Drupal\Core\Entity\EntityDisplayBase $display
*
* @return \stdClass
*/
function field_group_field_ui_form_params($form, EntityDisplayBase $display) {
$params = new stdClass();
$params->entity_type = $display->getTargetEntityTypeId();
$params->bundle = $display->getTargetBundle();
$params->mode = $display->getMode();
$params->context = field_group_get_context_from_display($display);
$params->groups = [];
$params->groups = field_group_info_groups($params->entity_type, $params->bundle, $params->context, $params->mode);
// Gather parenting data.
$params->parents = [];
foreach ($params->groups as $name => $group) {
foreach ($group->children as $child) {
// Field UI js sometimes can trigger an endless loop. Check if the parent
// of this field is not a child.
if ($child !== $group->parent_name) {
$params->parents[$child] = $name;
}
}
}
// Get possible regions.
// TODO remove the field layout part when it's remove from in core.
// see https://www.drupal.org/project/field_group/issues/3086019
$ds_info = $display->getThirdPartySettings('ds');
$field_layout_info = $display->getThirdPartySettings('field_layout');
/** @var \Drupal\Core\Layout\LayoutDefinition $layout */
if (!empty($field_layout_info) && isset($field_layout_info['id'])) {
$layout = \Drupal::service('plugin.manager.core.layout')->getDefinition($field_layout_info['id']);
$params->available_regions = $layout->getRegionNames();
$params->default_region = $layout->getDefaultRegion() ?: 'hidden';
}
elseif (!empty($ds_info['layout']['id'])) {
$layout = \Drupal::service('plugin.manager.core.layout')->getDefinition($ds_info['layout']['id']);
$params->available_regions = $layout->getRegionNames();
// Hidden is an available region too, as weird as it may seems.
$params->available_regions[] = 'hidden';
$params->default_region = $layout->getDefaultRegion() ?: 'hidden';
}
else {
$params->available_regions = ['content', 'hidden'];
$params->default_region = 'hidden';
}
return $params;
}
/**
* Helper function to get context from entity display.
*
* @param \Drupal\Core\Entity\EntityDisplayBase $display
*
* @return string
*/
function field_group_get_context_from_display(EntityDisplayBase $display) {
if ($display instanceof EntityFormDisplayInterface) {
return 'form';
}
elseif ($display instanceof EntityViewDisplayInterface) {
return 'view';
}
throw new LogicException('Unknown display object.');
}
/**
* Function to alter the display overview screens.
*/
function field_group_field_ui_display_form_alter(&$form, FormStateInterface $form_state) {
// Only start altering the form if we need to.
if (empty($form['#fields']) && empty($form['#extra'])) {
return;
}
$entity_display_form = $form_state->getBuildInfo()['callback_object'];
if (!$entity_display_form instanceof EntityDisplayFormBase) {
throw new InvalidArgumentException('Unknown callback object.');
}
$display = $entity_display_form->getEntity();
$params = field_group_field_ui_form_params($form, $display);
$form['#fieldgroups'] = array_keys($params->groups);
$form['#context'] = $display;
$table = &$form['fields'];
$form_state_values = $form_state->getValues();
$field_group_form_state = $form_state->get('field_group');
if ($field_group_form_state == NULL) {
$field_group_form_state = $params->groups;
}
$table['#parent_options'] = [];
// Extend available parenting options.
foreach ($field_group_form_state as $name => $group) {
$table['#parent_options'][$name] = $group->label;
}
// Update existing rows accordingly to the parents.
foreach (Element::children($table) as $name) {
$table[$name]['parent_wrapper']['parent']['#options'] = $table['#parent_options'];
// Inherit the value of the parent when default value is empty.
if (empty($table[$name]['parent_wrapper']['parent']['#default_value'])) {
$table[$name]['parent_wrapper']['parent']['#default_value'] = isset($params->parents[$name]) ? $params->parents[$name] : '';
}
}
$formatter_options = FormatterHelper::formatterOptions($params->context);
$refresh_rows = isset($form_state_values['refresh_rows']) ? $form_state_values['refresh_rows'] : (isset($form_state->getUserInput()['refresh_rows']) ? $form_state->getUserInput()['refresh_rows'] : NULL);
// Create the group rows and check actions.
foreach ($form['#fieldgroups'] as $name) {
$group = &$field_group_form_state[$name];
// Check the currently selected formatter, and merge persisted values for
// formatter settings for the group.
// This needs to be done first, so all fields are updated before creating form elements.
if (isset($refresh_rows) && $refresh_rows == $name) {
$settings = isset($form_state_values['fields'][$name]) ? $form_state_values['fields'][$name] : (isset($form_state->getUserInput()['fields'][$name]) ? $form_state->getUserInput()['fields'][$name] : NULL);
if (array_key_exists('settings_edit', $settings)) {
$group = $field_group_form_state[$name];
}
field_group_formatter_row_update($group, $settings);
}
// Save the group when the configuration is submitted.
if (!empty($form_state_values[$name . '_plugin_settings_update'])) {
field_group_formatter_settings_update($group, $form_state_values['fields'][$name]);
}
// After all updates are finished, let the form_state know.
$field_group_form_state[$name] = $group;
$settings = field_group_format_settings_form($group, $form, $form_state);
$id = strtr($name, '_', '-');
// A group cannot be selected as its own parent.
$parent_options = $table['#parent_options'];
$region = isset($group->region) && in_array($group->region, $params->available_regions) ? $group->region : $params->default_region;
unset($parent_options[$name]);
$table[$name] = [
'#attributes' => ['class' => ['draggable', 'field-group'], 'id' => $id],
'#row_type' => 'group',
'#region_callback' => 'field_group_display_overview_row_region',
'#js_settings' => ['rowHandler' => 'group'],
'human_name' => [
'#markup' => $group->label,
'#prefix' => '<span class="group-label">',
'#suffix' => '</span>',
],
'weight' => [
'#type' => 'textfield',
'#default_value' => $group->weight,
'#size' => 3,
'#attributes' => ['class' => ['field-weight']],
],
'parent_wrapper' => [
'parent' => [
'#type' => 'select',
'#options' => $parent_options,
'#empty_value' => '',
'#default_value' => isset($params->parents[$name]) ? $params->parents[$name] : '',
'#attributes' => ['class' => ['field-parent']],
'#parents' => ['fields', $name, 'parent'],
],
'hidden_name' => [
'#type' => 'hidden',
'#default_value' => $name,
'#attributes' => ['class' => ['field-name']],
],
],
'region' => [
'#type' => 'select',
'#options' => $entity_display_form->getRegionOptions(),
'#default_value' => $region,
'#attributes' => ['class' => ['field-region']],
],
];
// For view settings. Add a spacer cell. We can't use colspan because of the javascript .
if ($params->context == 'view') {
$table[$name] += [
'spacer' => [
'#markup' => '&nbsp;',
],
];
}
$table[$name] += [
'format' => [
'type' => [
'#type' => 'select',
'#options' => $formatter_options,
'#default_value' => $group->format_type,
'#attributes' => ['class' => ['field-group-type']],
],
],
];
$base_button = [
'#submit' => [
[$form_state->getBuildInfo()['callback_object'], 'multistepSubmit'],
],
'#ajax' => [
'callback' => [$form_state->getBuildInfo()['callback_object'], 'multistepAjax'],
'wrapper' => 'field-display-overview-wrapper',
'effect' => 'fade',
],
'#field_name' => $name,
];
if ($form_state->get('plugin_settings_edit') == $name) {
$table[$name]['format']['#cell_attributes'] = ['colspan' => 2];
$table[$name]['format']['format_settings'] = [
'#type' => 'container',
'#attributes' => ['class' => ['field-plugin-settings-edit-form']],
'#parents' => ['fields', $name, 'settings_edit_form'],
'#weight' => -5,
'label' => [
'#markup' => t('Field group format:') . ' <span class="formatter-name">' . $group->format_type . '</span>',
],
// Create a settings form where hooks can pick in.
'settings' => $settings,
'actions' => [
'#type' => 'actions',
'save_settings' => $base_button + [
'#type' => 'submit',
'#name' => $name . '_plugin_settings_update',
'#value' => t('Update'),
'#op' => 'update',
],
'cancel_settings' => $base_button + [
'#type' => 'submit',
'#name' => $name . '_plugin_settings_cancel',
'#value' => t('Cancel'),
'#op' => 'cancel',
// Do not check errors for the 'Cancel' button.
'#limit_validation_errors' => [],
],
],
];
$table[$name]['#attributes']['class'][] = 'field-formatter-settings-editing';
$table[$name]['format']['type']['#attributes']['class'] = ['visually-hidden'];
}
else {
// After saving, the settings are updated here aswell. First we create
// the element for the table cell.
$table[$name]['settings_summary'] = ['#markup' => ''];
if (!empty($group->format_settings)) {
$table[$name]['settings_summary'] = field_group_format_settings_summary($name, $group);
}
// Add the configure button.
$table[$name]['settings_edit'] = $base_button + [
'#type' => 'image_button',
'#name' => $name . '_group_settings_edit',
'#src' => 'core/misc/icons/787878/cog.svg',
'#attributes' => ['class' => ['field-plugin-settings-edit'], 'alt' => t('Edit')],
'#op' => 'edit',
// Do not check errors for the 'Edit' button, but make sure we get
// the value of the 'plugin type' select.
'#limit_validation_errors' => [['fields', $name, 'type']],
'#prefix' => '<div class="field-plugin-settings-edit-wrapper">',
'#suffix' => '</div>',
];
$delete_route = FieldgroupUi::getDeleteRoute($group);
$table[$name]['settings_edit']['#suffix'] .= Link::fromTextAndUrl(t('delete'), $delete_route)->toString();
}
$form_state->set('field_group', $field_group_form_state);
}
// Additional row: add new group.
$parent_options = $table['#parent_options'];
$form['#attached']['library'][] = 'field_group/field_ui';
array_unshift($form['actions']['submit']['#submit'], 'field_group_field_overview_submit');
// Create the settings for fieldgroup as vertical tabs (merged with DS).
field_group_field_ui_create_vertical_tabs($form, $form_state, $params);
// Show a warning if the user has not set up required containers.
if ($form['#fieldgroups']) {
$parent_requirements = [
'accordion-item' => [
'parent' => 'accordion',
'message' => 'Each Accordion item element needs to have a parent Accordion group element.',
],
];
// On display overview tabs need to be checked.
if (field_group_get_context_from_display($display) == 'view') {
$parent_requirements['tab'] = [
'parent' => 'tabs',
'message' => 'Each tab element needs to have a parent tabs group element.',
];
}
foreach ($form['#fieldgroups'] as $group_name) {
$group_check = field_group_load_field_group($group_name, $params->entity_type, $params->bundle, $params->context, $params->mode);
if (isset($parent_requirements[$group_check->format_type])) {
if (!$group_check->parent_name || field_group_load_field_group($group_check->parent_name, $params->entity_type, $params->bundle, $params->context, $params->mode)->format_type != $parent_requirements[$group_check->format_type]['parent']) {
\Drupal::messenger()->addMessage(t($parent_requirements[$group_check->format_type]['message']), 'warning', FALSE);
}
}
}
}
}
/**
* Create vertical tabs.
*/
function field_group_field_ui_create_vertical_tabs(&$form, &$form_state, $params) {
$form_state->set('field_group_params', $params);
$existing_group_config = \Drupal::configFactory()->listAll('field_group.' . $params->entity_type . '.' . $params->bundle);
$displays = [];
foreach ($existing_group_config as $config) {
$group = \Drupal::config($config)->get();
if ($group['context'] == $params->context && $group['mode'] == $params->mode) {
continue;
}
$displays[$group['context'] . '.' . $group['mode']] = $group['context'] . ':' . $group['mode'];
}
// No displays to clone.
if (empty($displays)) {
return;
}
// Add additional settings vertical tab.
if (!isset($form['additional_settings'])) {
$form['additional_settings'] = [
'#type' => 'vertical_tabs',
'#theme_wrappers' => ['vertical_tabs'],
'#prefix' => '<div>',
'#suffix' => '</div>',
'#tree' => TRUE,
];
}
// Add extra guidelines for webmaster.
$form['field_group'] = [
'#type' => 'details',
'#group' => 'additional_settings',
'#title' => t('Fieldgroups'),
'#description' => t('<p class="fieldgroup-help">Fields can be dragged into groups with unlimited nesting. Each fieldgroup format comes with a configuration form, specific for that format type.<br />Note that some formats come in pair. These types have a html wrapper to nest its fieldgroup children. E.g. Place accordion items into the accordion, vertical tabs in vertical tab group and horizontal tabs in the horizontal tab group. There is one exception to this rule, you can use a vertical tab without a wrapper when the additional settings tabs are available. E.g. node forms.</p>'),
'#collapsible' => TRUE,
'#open' => TRUE,
];
$form['field_group']['fieldgroup_clone'] = [
'#title' => t('Select source display'),
'#description' => t('Clone fieldgroups from selected display to the current display'),
'#type' => 'select',
'#options' => $displays,
'#default_value' => 'none',
];
$form['field_group']['fieldgroup_submit'] = [
'#type' => 'submit',
'#value' => t('Clone'),
'#validate' => ['field_group_field_ui_clone_field_groups_validate'],
'#submit' => ['field_group_field_ui_clone_field_groups'],
];
}
/**
* Returns the region to which a row in the 'Manage display' screen belongs.
*
* @param array $row
* A field or field_group row.
*
* @return String the current region.
*/
function field_group_display_overview_row_region($row) {
// We already cleaned region when building the form.
return $row['region']['#value'];
}
/**
* Submit handler for the overview screens.
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the form.
*/
function field_group_field_overview_submit($form, FormStateInterface $form_state) {
$form_values = $form_state->getValue('fields');
/**
* @var \Drupal\Core\Entity\EntityDisplayBase $display
*/
$display = $form['#context'];
$manager = Drupal::service('plugin.manager.field_group.formatters');
$entity_type = $display->get('targetEntityType');
$bundle = $display->get('bundle');
$mode = $display->get('mode');
$context = field_group_get_context_from_display($display);
// Load field layout info.
$field_group_params = $form_state->get('field_group_params');
$layout_regions = $field_group_params->available_regions;
$default_region = $field_group_params->default_region;
// Collect children.
$children = array_fill_keys($form['#fieldgroups'], []);
foreach ($form_values as $name => $value) {
if (!empty($value['parent'])) {
$children[$value['parent']][$name] = $name;
}
}
// Update existing groups.
$groups = field_group_info_groups($entity_type, $bundle, $context, $mode);
$field_group_form_state = $form_state->get('field_group');
if (!empty($field_group_form_state)) {
foreach ($form['#fieldgroups'] as $group_name) {
// Only save updated groups.
if (!isset($field_group_form_state[$group_name])) {
continue;
}
$group = $groups[$group_name];
$group->label = $field_group_form_state[$group_name]->label;
// Sometimes field UI freaks a bit if people drag to fast when switching
// nested values which results in an endless loop, so cleanup first.
// unset($children[$group_name][$form_values[$group_name]['parent']]);.
$group->children = array_keys($children[$group_name]);
$group->parent_name = $form_values[$group_name]['parent'];
$group->weight = $form_values[$group_name]['weight'];
// If region is changed, make sure the group ends up in an existing region.
$group->region = !in_array($form_values[$group_name]['region'], $layout_regions) ? $default_region : $form_values[$group_name]['region'];
$old_format_type = $group->format_type;
$group->format_type = isset($form_values[$group_name]['format']['type']) ? $form_values[$group_name]['format']['type'] : 'visible';
if (isset($field_group_form_state[$group_name]->format_settings)) {
$group->format_settings = $field_group_form_state[$group_name]->format_settings;
}
// If the format type is changed, make sure we have all required format settings.
if ($group->format_type != $old_format_type) {
$group->format_settings += $manager->getDefaultSettings($group->format_type, $context);
}
/** @var EntityFormInterface $entity_form */
$entity_form = $form_state->getFormObject();
/** @var EntityDisplayInterface $display */
$display = $entity_form->getEntity();
field_group_group_save($group, $display);
}
}
\Drupal::cache()->invalidate('field_groups');
}
/**
* Creates a form for field_group formatters.
*
* @param Object $group
* The FieldGroup object.
*/
function field_group_format_settings_form(&$group, $form, $form_state) {
$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,
]);
if ($plugin) {
return $plugin->settingsForm($form, $form_state);
}
return [];
}
/**
* Update the row so that the group variables are updated.
* The rendering of the elements needs the updated defaults.
*
* @param Object $group
* @param array $settings
*/
function field_group_formatter_row_update(& $group, $settings) {
// If the row has changed formatter type, update the group object.
if (!empty($settings['format']['type']) && $settings['format']['type'] != $group->format_type) {
$group->format_type = $settings['format']['type'];
field_group_formatter_settings_update($group, $settings);
}
}
/**
* Update handler for field_group configuration settings.
*
* @param Object $group
* The group object.
* @param array $settings
* Configuration settings.
*/
function field_group_formatter_settings_update(& $group, $settings) {
// For format changes we load the defaults.
if (empty($settings['settings_edit_form']['settings'])) {
$group->format_settings = Drupal::service('plugin.manager.field_group.formatters')->getDefaultSettings($group->format_type, $group->context);
}
else {
$group->format_type = $settings['format']['type'];
$group->label = $settings['settings_edit_form']['settings']['label'];
$group->format_settings = $settings['settings_edit_form']['settings'];
}
}
/**
* Creates a summary for the field format configuration summary.
*
* @param string $group_name
* The name of the group.
* @param Object $group
* The group object.
*
* @return Array ready to be rendered.
*/
function field_group_format_settings_summary($group_name, $group) {
$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,
]);
if ($plugin) {
$summary = $plugin->settingsSummary();
}
else {
$summary = '';
}
return [
'#markup' => '<div class="field-plugin-summary">' . implode('<br />', $summary) . '</div>',
'#cell_attributes' => ['class' => ['field-plugin-summary-cell']],
];
}
/**
* Validate handler to validate saving existing fieldgroups from one view mode or form to another.
*/
function field_group_field_ui_clone_field_groups_validate($form, FormStateInterface $form_state) {
$form_state_values = $form_state->getValues();
$field_group_params = $form_state->get('field_group_params');
list($context, $mode) = explode('.', $form_state_values['fieldgroup_clone']);
$source_groups = field_group_info_groups($field_group_params->entity_type, $field_group_params->bundle, $context, $mode);
// Check for types are not known in current mode.
if ($field_group_params->context != 'form') {
$non_existing_types = [];
}
else {
$non_existing_types = ['html_element'];
}
foreach ($source_groups as $key => $group) {
if (in_array($group->format_type, $non_existing_types)) {
unset($source_groups[$key]);
\Drupal::messenger()->addMessage(t('Skipping @group because this type does not exist in current mode', ['@group' => $group->label]), 'warning');
}
}
if (empty($source_groups)) {
// Report error found with selection.
$form_state->setErrorByName('additional_settings][fieldgroup_clone', t('No field groups were found in selected view mode.'));
return;
}
$form_state->set('#source_groups', $source_groups);
}
/**
* Submit handler to save existing fieldgroups from one view mode or form to another.
*/
function field_group_field_ui_clone_field_groups($form, FormStateInterface $form_state) {
$fields = array_keys($form_state->getValue('fields'));
$source_groups = $form_state->get('#source_groups');
if ($source_groups) {
$field_group_params = $form_state->get('field_group_params');
foreach ($source_groups as $source_group) {
if (in_array($source_group->group_name, $fields)) {
\Drupal::messenger()->addMessage(t('Fieldgroup @group is not cloned since a group already exists with the same name.', ['@group' => $source_group->group_name]), 'warning');
continue;
}
$source_group->context = $field_group_params->context;
$source_group->mode = $field_group_params->mode;
$source_group->children = [];
field_group_group_save($source_group);
\Drupal::messenger()->addMessage(t('Fieldgroup @group cloned successfully.', ['@group' => $source_group->group_name]));
}
}
}
(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;
}
}
<?php
namespace Drupal\field_group\Plugin\field_group\FieldGroupFormatter;
use Drupal\Component\Utility\Html;
use Drupal\field_group\FieldGroupFormatterBase;
/**
* Plugin implementation of the 'tab' formatter.
*
* @FieldGroupFormatter(
* id = "tab",
* label = @Translation("Tab"),
* description = @Translation("This fieldgroup renders the content as a tab."),
* format_types = {
* "open",
* "closed",
* },
* supported_contexts = {
* "form",
* "view",
* }
* )
*/
class Tab extends FieldGroupFormatterBase {
/**
* {@inheritdoc}
*/
public function process(&$element, $processed_object) {
// Keep using preRender parent for BC.
parent::preRender($element, $processed_object);
$add = [
'#type' => 'details',
'#title' => $this->getLabel(),
'#description' => $this->getSetting('description'),
'#group' => $this->group->parent_name,
];
if ($this->getSetting('id')) {
$add['#id'] = Html::getUniqueId($this->getSetting('id'));
}
else {
$add['#id'] = Html::getUniqueId('edit-' . $this->group->group_name);
}
$classes = $this->getClasses();
if (!empty($classes)) {
$element += [
'#attributes' => ['class' => $classes],
];
}
if ($this->getSetting('formatter') == 'open') {
$element['#open'] = TRUE;
}
if ($this->getSetting('required_fields')) {
$element['#attached']['library'][] = 'field_group/formatter.tabs';
$element['#attached']['library'][] = 'field_group/core';
}
$element += $add;
}
/**
* {@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['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 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\Core\Form\FormState;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\VerticalTabs;
use Drupal\field_group\Element\HorizontalTabs;
use Drupal\field_group\FieldGroupFormatterBase;
/**
* Plugin implementation of the 'horizontal_tabs' formatter.
*
* @FieldGroupFormatter(
* id = "tabs",
* label = @Translation("Tabs"),
* description = @Translation("This fieldgroup renders child groups in its own tabs wrapper."),
* supported_contexts = {
* "form",
* "view",
* }
* )
*/
class Tabs extends FieldGroupFormatterBase {
/**
* {@inheritdoc}
*/
public function process(&$element, $processed_object) {
// Keep using preRender parent for BC.
parent::preRender($element, $processed_object);
$element += [
'#prefix' => '<div class=" ' . implode(' ', $this->getClasses()) . '">',
'#suffix' => '</div>',
'#tree' => TRUE,
'#parents' => [$this->group->group_name],
'#default_tab' => '',
];
if ($this->getSetting('id')) {
$element['#id'] = Html::getUniqueId($this->getSetting('id'));
}
// By default tabs don't have titles but you can override it in the theme.
if ($this->getLabel()) {
$element['#title'] = $this->getLabel();
}
$element += [
'#type' => $this->getSetting('direction') . '_tabs',
'#theme_wrappers' => [$this->getSetting('direction') . '_tabs'],
];
}
/**
* {@inheritdoc}
*/
public function preRender(&$element, $rendering_object) {
$this->process($element, $rendering_object);
if ($this->getSetting('direction') == 'vertical') {
$form_state = new FormState();
$complete_form = [];
$element = VerticalTabs::processVerticalTabs($element, $form_state, $complete_form);
}
else {
$form_state = new FormState();
$complete_form = [];
$element = HorizontalTabs::processHorizontalTabs($element, $form_state, $complete_form);
}
// Make sure the group has 1 child. This is needed to succeed at form_pre_render_vertical_tabs().
// Skipping this would force us to move all child groups to this array, making it an un-nestable.
$element['group']['#groups'][$this->group->group_name] = [0 => []];
$element['group']['#groups'][$this->group->group_name]['#group_exists'] = TRUE;
}
/**
* {@inheritdoc}
*/
public function settingsForm() {
$form = parent::settingsForm();
$form['direction'] = [
'#title' => $this->t('Direction'),
'#type' => 'select',
'#options' => [
'vertical' => $this->t('Vertical'),
'horizontal' => $this->t('Horizontal'),
],
'#default_value' => $this->getSetting('direction'),
'#weight' => 1,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
$summary[] = $this->t('Direction: @direction',
['@direction' => $this->getSetting('direction')]
);
return $summary;
}
/**
* {@inheritdoc}
*/
public static function defaultContextSettings($context) {
return [
'direction' => 'vertical',
] + parent::defaultContextSettings($context);
}
/**
* {@inheritdoc}
*/
public function getClasses() {
$classes = parent::getClasses();
$classes[] = 'field-group-' . $this->group->format_type . '-wrapper';
return $classes;
}
}
<?php
namespace Drupal\field_group\Routing;
use Symfony\Component\Routing\Route;
use Drupal\Core\ParamConverter\ParamConverterInterface;
/**
* Parameter converter for upcasting fieldgroup config ids to fieldgroup object.
*/
class FieldGroupConverter implements ParamConverterInterface {
/**
* {@inheritdoc}
*/
public function applies($definition, $name, Route $route) {
return isset($definition['type']) && $definition['type'] == 'field_group';
}
/**
* {@inheritdoc}
*/
public function convert($value, $definition, $name, array $defaults) {
$identifiers = explode('.', $value);
if (count($identifiers) != 5) {
return;
}
return field_group_load_field_group($identifiers[4], $identifiers[0], $identifiers[1], $identifiers[2], $identifiers[3]);
}
}
<?php
namespace Drupal\field_group\Routing;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Subscriber for Field group routes.
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a RouteSubscriber object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
// Create fieldgroup routes for every entity.
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
$defaults = [];
if ($route_name = $entity_type->get('field_ui_base_route')) {
// Try to get the route from the current collection.
if (!$entity_route = $collection->get($route_name)) {
continue;
}
$path = $entity_route->getPath();
$options = $entity_route->getOptions();
// Special parameter used to easily recognize all Field UI routes.
$options['_field_ui'] = TRUE;
if (($bundle_entity_type = $entity_type->getBundleEntityType()) && $bundle_entity_type !== 'bundle') {
$options['parameters'][$entity_type->getBundleEntityType()] = [
'type' => 'entity:' . $entity_type->getBundleEntityType(),
];
}
$options['parameters']['field_group'] = [
'type' => 'field_group',
'entity_type' => $entity_type->getBundleEntityType(),
];
$defaults_delete = [
'entity_type_id' => $entity_type_id,
'_form' => '\Drupal\field_group\Form\FieldGroupDeleteForm',
];
$defaults_add = [
'entity_type_id' => $entity_type_id,
'_form' => '\Drupal\field_group\Form\FieldGroupAddForm',
'_title' => 'Add field group',
];
// If the entity type has no bundles and it doesn't use {bundle} in its
// admin path, use the entity type.
if (strpos($path, '{bundle}') === FALSE) {
$defaults_add['bundle'] = !$entity_type->hasKey('bundle') ? $entity_type_id : '';
$defaults_delete['bundle'] = $defaults_add['bundle'];
}
// Routes to delete field groups.
$route = new Route(
"$path/form-display/{field_group_name}/delete",
['context' => 'form'] + $defaults_delete,
['_permission' => 'administer ' . $entity_type_id . ' form display'],
$options
);
$collection->add("field_ui.field_group_delete_$entity_type_id.form_display", $route);
$route = new Route(
"$path/form-display/{form_mode_name}/{field_group_name}/delete",
['context' => 'form'] + $defaults_delete,
['_permission' => 'administer ' . $entity_type_id . ' form display'],
$options
);
$collection->add("field_ui.field_group_delete_$entity_type_id.form_display.form_mode", $route);
$route = new Route(
"$path/display/{field_group_name}/delete",
['context' => 'view'] + $defaults_delete,
['_permission' => 'administer ' . $entity_type_id . ' display'],
$options
);
$collection->add("field_ui.field_group_delete_$entity_type_id.display", $route);
$route = new Route(
"$path/display/{view_mode_name}/{field_group_name}/delete",
['context' => 'view'] + $defaults_delete,
['_permission' => 'administer ' . $entity_type_id . ' display'],
$options
);
$collection->add("field_ui.field_group_delete_$entity_type_id.display.view_mode", $route);
// Routes to add field groups.
$route = new Route(
"$path/form-display/add-group",
['context' => 'form'] + $defaults_add,
['_permission' => 'administer ' . $entity_type_id . ' form display'],
$options
);
$collection->add("field_ui.field_group_add_$entity_type_id.form_display", $route);
$route = new Route(
"$path/form-display/{form_mode_name}/add-group",
['context' => 'form'] + $defaults_add,
['_permission' => 'administer ' . $entity_type_id . ' form display'],
$options
);
$collection->add("field_ui.field_group_add_$entity_type_id.form_display.form_mode", $route);
$route = new Route(
"$path/display/add-group",
['context' => 'view'] + $defaults_add,
['_permission' => 'administer ' . $entity_type_id . ' display'],
$options
);
$collection->add("field_ui.field_group_add_$entity_type_id.display", $route);
$route = new Route(
"$path/display/{view_mode_name}/add-group",
['context' => 'view'] + $defaults_add,
['_permission' => 'administer ' . $entity_type_id . ' display'],
$options
);
$collection->add("field_ui.field_group_add_$entity_type_id.display.view_mode", $route);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// $events = parent::getSubscribedEvents();
// Come after field_ui, config_translation.
$events[RoutingEvents::ALTER] = ['onAlterRoutes', -210];
return $events;
}
}
{#
/**
* @file
* Default theme implementation for a fieldgroup accordion item.
*
* Available variables:
* - title: Title of the group.
* - children: The children of the group.
* - label_attributes: A list of HTML attributes for the label.
* - attributes: A list of HTML attributes for the group wrapper.
*
* @see template_preprocess_field_group_accordion()
*
* @ingroup themeable
*/
#}
{%
set label_classes = [
'field-group-format-toggler',
'accordion-item',
open ? 'field-group-accordion-active',
]
%}
{%
set classes = [
'field-group-format-wrapper',
]
%}
<h3 {{ label_attributes.addClass(label_classes) }}>
<a href="#">{{ title }}</a>
</h3>
<div {{ attributes.addClass(classes) }}>
{% if description %}<div class="description"></div>{% endif %}
{{children}}
</div>
\ No newline at end of file
{#
/**
* @file
* Default theme implementation for a fieldgroup accordion item.
*
* Available variables:
* - children: The children of the group.
* - attributes: A list of HTML attributes for the group wrapper.
*
* @see template_preprocess_field_group_accordion()
*
* @ingroup themeable
*/
#}
{%
set classes = [
'field-group-accordion-wrapper',
]
%}
<div {{ attributes.addClass(classes) }}>{{ children }}</div>
{#
/**
* @file
* Default theme implementation for a fieldgroup html element.
*
* Available variables:
* - title: Title of the group.
* - title_element: Element to wrap the title.
* - children: The children of the group.
* - wrapper_element: The html element to use
* - attributes: A list of HTML attributes for the group wrapper.
*
* @see template_preprocess_field_group_html_element()
*
* @ingroup themeable
*/
#}
<{{ wrapper_element }} {{ attributes }}>
{% if title %}
<{{ title_element }}{{ title_attributes }}>{{ title }}</{{ title_element }}>
{% endif %}
{% if collapsible %}
<div class="field-group-wrapper">
{% endif %}
{{children}}
{% if collapsible %}
</div>
{% endif %}
</{{ wrapper_element }}>
{#
/**
* @file
* Default theme implementation for horizontal tabs.
*
* Available variables
* - attributes: A list of HTML attributes for the wrapper element.
* - children: The rendered children.
*
* @see template_preprocess_horizontal_tabs()
*
* @ingroup themeable
*/
#}
<div data-horizontal-tabs-panes{{ attributes }}>{{ children }}</div>
<?php
/**
* @file
* Preprocessors for fieldgroup elements.
*/
use Drupal\Core\Template\Attribute;
/**
* Prepares variables for horizontal tabs templates.
*
* Default template: horizontal-tabs.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties and children of
* the details element. Properties used: #children.
*/
function template_preprocess_horizontal_tabs(array &$variables) {
$element = $variables['element'];
$variables['children'] = (!empty($element['#children'])) ? $element['#children'] : '';
}
/**
* Prepares variables for fieldgroup accordion templates.
*
* Default template: field-group-accordion.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties and children of
* the accordion element. Properties used: #children.
*/
function template_preprocess_field_group_accordion(array &$variables) {
$element = $variables['element'];
$variables['children'] = (!empty($element['#children'])) ? $element['#children'] : '';
}
/**
* Prepares variables for fieldgroup accordion item templates.
*
* Default template: field-group-accordion-item.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties and children of
* the accordion item element.
*/
function template_preprocess_field_group_accordion_item(array &$variables) {
$element = $variables['element'];
if (!empty($element['#title'])) {
$variables['title'] = $element['#title'];
}
if (!empty($element['#description'])) {
$variables['description'] = $element['#description'];
}
$variables['open'] = $element['#open'];
$variables['label_attributes'] = new Attribute();
$variables['children'] = (!empty($element['#children'])) ? $element['#children'] : '';
}
/**
* Prepares variables for fieldgroup html element templates.
*
* Default template: field-group-html-element.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties and children of
* the html element.
*/
function template_preprocess_field_group_html_element(array &$variables) {
$element = $variables['element'];
if (!empty($element['#title']) && !empty($element['#title_element'])) {
$variables['title_element'] = $element['#title_element'];
$variables['title'] = $element['#title'];
$variables['title_attributes'] = $element['#title_attributes'];
}
$variables['collapsible'] = (!empty($element['#effect']) && $element['#effect'] !== 'none');
$variables['wrapper_element'] = $element['#wrapper_element'];
$variables['attributes'] = $element['#attributes'];
$variables['children'] = (!empty($element['#children'])) ? $element['#children'] : '';
}
name: 'Field Group Test'
description: 'Test module for Field Group'
core_version_requirement: ^8.8 || ^9
package: 'Fields'
type: module
hidden: TRUE
# Information added by Drupal.org packaging script on 2020-06-10
version: '8.x-3.1'
project: 'field_group'
datestamp: 1591772570
<?php
/**
* @file
* Fieldgroup test module.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Implements hook_entity_field_access().
*/
function field_group_test_entity_field_access($operation,
FieldDefinitionInterface $field_definition,
AccountInterface $account,
FieldItemListInterface $items = NULL) {
// Set access to false for field_no_access.
if ($operation == 'view' && $field_definition->getName() == 'field_no_access') {
return AccessResult::forbidden();
}
return AccessResult::neutral();
}
<?php
namespace Drupal\Tests\field_group\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
/**
* Tests for displaying entities.
*
* @group field_group
*/
class EntityDisplayTest extends BrowserTestBase {
use FieldGroupTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'field_test',
'field_ui',
'field_group',
'field_group_test'
];
/**
* The node type id.
*
* @var string
*/
protected $type;
/**
* A node to use for testing.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Create test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node fields',
'administer node form display',
'administer node display',
'bypass node access',
]);
$this->drupalLogin($admin_user);
// Create content type, with underscores.
$type_name = strtolower($this->randomMachineName(8)) . '_test';
$type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
$this->type = $type->id();
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
$display = \Drupal::entityTypeManager()
->getStorage('entity_view_display')
->load('node.' . $type_name . '.default');
// Create a node.
$node_values = ['type' => $type_name];
// Create test fields.
foreach (['field_test', 'field_test_2', 'field_no_access'] as $field_name) {
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'type' => 'test_field',
]);
$field_storage->save();
$instance = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => $type_name,
'label' => $this->randomMachineName(),
]);
$instance->save();
// Assign a test value for the field.
$node_values[$field_name][0]['value'] = mt_rand(1, 127);
// Set the field visible on the display object.
$display_options = [
'label' => 'above',
'type' => 'field_test_default',
'settings' => [
'test_formatter_setting' => $this->randomMachineName(),
],
];
$display->setComponent($field_name, $display_options);
}
// Save display + create node.
$display->save();
$this->node = $this->drupalCreateNode($node_values);
}
/**
* Test field access for field groups.
*/
public function testFieldAccess() {
$data = [
'label' => 'Wrapper',
'children' => [
0 => 'field_no_access',
],
'format_type' => 'html_element',
'format_settings' => [
'element' => 'div',
'id' => 'wrapper-id',
],
];
$this->createGroup('node', $this->type, 'view', 'default', $data);
$this->drupalGet('node/' . $this->node->id());
// Test if group is not shown.
$this->assertEmpty($this->xpath("//div[contains(@id, 'wrapper-id')]"), t('Div that contains fields with no access is not shown.'));
}
/**
* Test the html element formatter.
*/
public function testHtmlElement() {
$data = [
'weight' => '1',
'children' => [
0 => 'field_test',
1 => 'body',
],
'label' => 'Link',
'format_type' => 'html_element',
'format_settings' => [
'label' => 'Link',
'element' => 'div',
'id' => 'wrapper-id',
'classes' => 'test-class',
],
];
$group = $this->createGroup('node', $this->type, 'view', 'default', $data);
// $groups = field_group_info_groups('node', 'article', 'view', 'default', TRUE);.
$this->drupalGet('node/' . $this->node->id());
// Test group ids and classes.
$this->assertCount(1, $this->xpath("//div[contains(@id, 'wrapper-id')]"), 'Wrapper id set on wrapper div');
$this->assertCount(1, $this->xpath("//div[contains(@class, 'test-class')]"), 'Test class set on wrapper div, class="' . $group->group_name . ' test-class');
// Test group label.
$this->assertSession()->responseNotContains('<h3><span>' . $data['label'] . '</span></h3>');
// Set show label to true.
$group->format_settings['show_label'] = TRUE;
$group->format_settings['label_element'] = 'h3';
$group->format_settings['label_element_classes'] = 'my-label-class';
field_group_group_save($group);
$this->drupalGet('node/' . $this->node->id());
$this->assertSession()->responseContains('<h3 class="my-label-class">' . $data['label'] . '</h3>');
// Change to collapsible with blink effect.
$group->format_settings['effect'] = 'blink';
$group->format_settings['speed'] = 'fast';
field_group_group_save($group);
$this->drupalGet('node/' . $this->node->id());
$this->assertCount(1, $this->xpath("//div[contains(@class, 'speed-fast')]"), 'Speed class is set');
$this->assertCount(1, $this->xpath("//div[contains(@class, 'effect-blink')]"), 'Effect class is set');
}
/**
* Test the fieldset formatter.
*/
public function testFieldset() {
$data = [
'weight' => '1',
'children' => [
0 => 'field_test',
1 => 'body',
],
'label' => 'Test Fieldset',
'format_type' => 'fieldset',
'format_settings' => [
'id' => 'fieldset-id',
'classes' => 'test-class',
'description' => 'test description',
],
];
$this->createGroup('node', $this->type, 'view', 'default', $data);
$this->drupalGet('node/' . $this->node->id());
// Test group ids and classes.
$this->assertCount(1, $this->xpath("//fieldset[contains(@id, 'fieldset-id')]"), 'Correct id set on the fieldset');
$this->assertCount(1, $this->xpath("//fieldset[contains(@class, 'test-class')]"), 'Test class set on the fieldset');
}
/**
* Test the tabs formatter.
*/
public function testTabs() {
$data = [
'label' => 'Tab 1',
'weight' => '1',
'children' => [
0 => 'field_test',
],
'format_type' => 'tab',
'format_settings' => [
'label' => 'Tab 1',
'classes' => 'test-class',
'description' => '',
'formatter' => 'open',
],
];
$first_tab = $this->createGroup('node', $this->type, 'view', 'default', $data);
$data = [
'label' => 'Tab 2',
'weight' => '1',
'children' => [
0 => 'field_test_2',
],
'format_type' => 'tab',
'format_settings' => [
'label' => 'Tab 1',
'classes' => 'test-class-2',
'description' => 'description of second tab',
'formatter' => 'closed',
],
];
$second_tab = $this->createGroup('node', $this->type, 'view', 'default', $data);
$data = [
'label' => 'Tabs',
'weight' => '1',
'children' => [
0 => $first_tab->group_name,
1 => $second_tab->group_name,
],
'format_type' => 'tabs',
'format_settings' => [
'direction' => 'vertical',
'label' => 'Tab 1',
'classes' => 'test-class-wrapper',
],
];
$tabs_group = $this->createGroup('node', $this->type, 'view', 'default', $data);
$this->drupalGet('node/' . $this->node->id());
// Test properties.
$this->assertCount(1, $this->xpath("//div[contains(@class, 'test-class-wrapper')]"), 'Test class set on tabs wrapper');
$this->assertCount(1, $this->xpath("//details[contains(@class, 'test-class-2')]"), 'Test class set on second tab');
$this->assertSession()->responseContains('<div class="details-description">description of second tab</div>');
// Test if correctly nested.
$this->assertCount(2, $this->xpath("//div[contains(@class, 'test-class-wrapper')]//details[contains(@class, 'test-class')]"), 'First tab is displayed as child of the wrapper.');
$this->assertCount(1, $this->xpath("//div[contains(@class, 'test-class-wrapper')]//details[contains(@class, 'test-class-2')]"), 'Second tab is displayed as child of the wrapper.');
// Test if it's a vertical tab.
$this->assertCount(1, $this->xpath('//div[@data-vertical-tabs-panes=""]'), 'Tabs are shown vertical.');
// Switch to horizontal.
$tabs_group->format_settings['direction'] = 'horizontal';
field_group_group_save($tabs_group);
$this->drupalGet('node/' . $this->node->id());
// Test if it's a horizontal tab.
$this->assertCount(1, $this->xpath('//div[@data-horizontal-tabs-panes=""]'), 'Tabs are shown horizontal.');
}
/**
* Test the accordion formatter.
*/
public function testAccordion() {
$data = [
'label' => 'Accordion item 1',
'weight' => '1',
'children' => [
0 => 'field_test',
],
'format_type' => 'accordion_item',
'format_settings' => [
'label' => 'Accordion item 1',
'classes' => 'test-class',
'formatter' => 'closed',
],
];
$first_item = $this->createGroup('node', $this->type, 'view', 'default', $data);
$data = [
'label' => 'Accordion item 2',
'weight' => '1',
'children' => [
0 => 'field_test_2',
],
'format_type' => 'accordion_item',
'format_settings' => [
'label' => 'Tab 2',
'classes' => 'test-class-2',
'formatter' => 'open',
],
];
$second_item = $this->createGroup('node', $this->type, 'view', 'default', $data);
$data = [
'label' => 'Accordion',
'weight' => '1',
'children' => [
0 => $first_item->group_name,
1 => $second_item->group_name,
],
'format_type' => 'accordion',
'format_settings' => [
'label' => 'Tab 1',
'classes' => 'test-class-wrapper',
'effect' => 'bounceslide',
],
];
$this->createGroup('node', $this->type, 'view', 'default', $data);
$this->drupalGet('node/' . $this->node->id());
// Test properties.
$this->assertCount(1, $this->xpath("//div[contains(@class, 'test-class-wrapper')]"), 'Test class set on tabs wrapper');
$this->assertCount(1, $this->xpath("//div[contains(@class, 'effect-bounceslide')]"), 'Correct effect is set on the accordion');
$this->assertCount(3, $this->xpath("//div[contains(@class, 'test-class')]"), 'Accordion item with test-class is shown');
$this->assertCount(1, $this->xpath("//div[contains(@class, 'test-class-2')]"), 'Accordion item with test-class-2 is shown');
$this->assertCount(1, $this->xpath("//h3[contains(@class, 'field-group-accordion-active')]"), 'Accordion item 2 was set active');
// Test if correctly nested.
$this->assertCount(2, $this->xpath("//div[contains(@class, 'test-class-wrapper')]//div[contains(@class, 'test-class')]"), 'First item is displayed as child of the wrapper.');
$this->assertCount(1, $this->xpath("//div[contains(@class, 'test-class-wrapper')]//div[contains(@class, 'test-class-2')]"), 'Second item is displayed as child of the wrapper.');
}
}
<?php
namespace Drupal\Tests\field_group\Functional;
use Drupal;
/**
* Provides common functionality for the FieldGroup test classes.
*/
trait FieldGroupTestTrait {
/**
* Create a new group.
*
* @param string $entity_type
* The entity type as string.
* @param string $bundle
* The bundle of the enity type.
* @param string $context
* The context for the group.
* @param string $mode
* The view/form mode.
* @param array $data
* Data for the field group.
*
* @return object
* An object that represents the field group.
*/
protected function createGroup($entity_type, $bundle, $context, $mode, array $data) {
if (!isset($data['format_settings'])) {
$data['format_settings'] = [];
}
$data['format_settings'] += Drupal::service('plugin.manager.field_group.formatters')->getDefaultSettings($data['format_type'], $context);
$group_name_without_prefix = isset($data['group_name']) && is_string($data['group_name'])
? preg_replace('/^group_/', '', $data['group_name'])
: mb_strtolower($this->randomMachineName());
$group_name = 'group_' . $group_name_without_prefix;
$field_group = (object) [
'group_name' => $group_name,
'entity_type' => $entity_type,
'bundle' => $bundle,
'mode' => $mode,
'context' => $context,
'children' => isset($data['children']) ? $data['children'] : [],
'parent_name' => isset($data['parent']) ? $data['parent'] : '',
'weight' => isset($data['weight']) ? $data['weight'] : 0,
'label' => isset($data['label']) ? $data['label'] : $this->randomString(8),
'format_type' => $data['format_type'],
'format_settings' => $data['format_settings'],
'region' => 'content',
];
field_group_group_save($field_group);
return $field_group;
}
}
<?php
namespace Drupal\Tests\field_group\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Test field_group without field_ui.
*
* @group field_group
*/
class FieldGroupWithoutFieldUiTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['field_group', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Test that local actions show up without field ui enabled.
*/
public function testLocalActions() {
// Local actions of field_group should not depend on field_ui.
// @see https://www.drupal.org/node/2719569
$this->placeBlock('local_actions_block', ['id' => 'local_actions_block']);
$this->drupalGet(Url::fromRoute('user.login'));
}
}
<?php
namespace Drupal\Tests\field_group\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests for managing display of entities.
*
* @group field_group
*/
class ManageDisplayTest extends BrowserTestBase {
use FieldGroupTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'field_ui', 'field_group'];
/**
* Content type id.
*
* @var string
*/
protected $type;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Create test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node fields',
'administer node form display',
'administer node display',
'bypass node access',
]);
$this->drupalLogin($admin_user);
// Create content type, with underscores.
$type_name = 'll4ma_test';
$type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
$this->type = $type->id();
}
/**
* Test the creation a group on the article content type.
*/
public function testCreateGroup() {
// Create random group name.
$group_label = $this->randomString(8);
$group_name_input = mb_strtolower($this->randomMachineName());
$group_name = 'group_' . $group_name_input;
$group_formatter = 'details';
// Setup new group.
$group = [
'group_formatter' => $group_formatter,
'label' => $group_label,
];
$add_form_display = 'admin/structure/types/manage/' . $this->type . '/form-display/add-group';
$this->drupalPostForm($add_form_display, $group, 'Save and continue');
$this->assertSession()->pageTextContains('Machine-readable name field is required.');
// Add required field to form.
$group['group_name'] = $group_name_input;
// Add new group on the 'Manage form display' page.
$this->drupalPostForm($add_form_display, $group, 'Save and continue');
$this->drupalPostForm(NULL, [], 'Create group');
$this->assertSession()->responseContains(t('New group %label successfully created.', ['%label' => $group_label]));
// Test if group is in the $groups array.
$this->group = field_group_load_field_group($group_name, 'node', $this->type, 'form', 'default');
$this->assertNotNull($group, 'Group was loaded');
// Test if region key is set.
$this->assertEquals('hidden', $this->group->region);
// Add new group on the 'Manage display' page.
$this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/display/add-group', $group, 'Save and continue');
$this->drupalPostForm(NULL, [], 'Create group');
$this->assertSession()->responseContains(t('New group %label successfully created.', ['%label' => $group_label]));
// Test if group is in the $groups array.
$loaded_group = field_group_load_field_group($group_name, 'node', $this->type, 'view', 'default');
$this->assertNotNull($loaded_group, 'Group was loaded');
}
/**
* Delete a group.
*/
public function testDeleteGroup() {
$data = [
'format_type' => 'fieldset',
'label' => 'testing',
];
$group = $this->createGroup('node', $this->type, 'form', 'default', $data);
$this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/form-display/' . $group->group_name . '/delete', [], 'Delete');
$this->assertSession()->responseContains(t('The group %label has been deleted from the %type content type.', ['%label' => $group->label, '%type' => $this->type]));
// Test that group is not in the $groups array.
\Drupal::entityTypeManager()
->getStorage('entity_form_display')
->resetCache();
$loaded_group = field_group_load_field_group($group->group_name, 'node', $this->type, 'form', 'default');
$this->assertNull($loaded_group, 'Group not found after deleting');
$data = [
'format_type' => 'fieldset',
'label' => 'testing',
];
$group = $this->createGroup('node', $this->type, 'view', 'default', $data);
$this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/display/' . $group->group_name . '/delete', [], t('Delete'));
$this->assertSession()->responseContains(t('The group %label has been deleted from the %type content type.', ['%label' => $group->label, '%type' => $this->type]));
// Test that group is not in the $groups array.
\Drupal::entityTypeManager()
->getStorage('entity_view_display')
->resetCache();
$loaded_group = field_group_load_field_group($group->group_name, 'node', $this->type, 'view', 'default');
$this->assertNull($loaded_group, 'Group not found after deleting');
}
/**
* Nest a field underneath a group.
*/
public function testNestField() {
$data = [
'format_type' => 'fieldset',
];
$group = $this->createGroup('node', $this->type, 'form', 'default', $data);
$edit = [
'fields[body][parent]' => $group->group_name,
];
$this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/form-display', $edit, 'Save');
$this->assertSession()->responseContains('Your settings have been saved.');
$group = field_group_load_field_group($group->group_name, 'node', $this->type, 'form', 'default');
$this->assertTrue(in_array('body', $group->children), t('Body is a child of %group', ['%group' => $group->group_name]));
}
}
<?php
namespace Drupal\Tests\field_group\FunctionalJavascript;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\field_group\Functional\FieldGroupTestTrait;
/**
* Test field_group user interface.
*
* @group field_group
*/
class FieldGroupUiTest extends WebDriverTestBase {
use FieldGroupTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'field_ui', 'field_group'];
/**
* The current tested node type
*
* @var string
*/
protected $nodeType;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Create test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node fields',
'administer node form display',
'administer node display',
'bypass node access',
]);
$this->drupalLogin($admin_user);
// Create content type, with underscores.
$type_name = mb_strtolower($this->randomMachineName(8)) . '_test';
$type = NodeType::create([
'name' => $type_name,
'type' => $type_name,
]);
$type->save();
$this->nodeType = $type->id();
}
/**
* Test creation and editing trough the UI.
*/
public function testCreateAndEdit() {
foreach (['test_1', 'test_2'] as $name) {
$this->drupalGet('admin/structure/types/manage/' . $this->nodeType . '/form-display/add-group');
$page = $this->getSession()->getPage();
// Type the label to activate the machine name field and the edit button.
$page->fillField('group_formatter', 'details');
$page->fillField('label', 'Test 1');
// Wait for the machine name widget to be activated.
$this->assertSession()->waitForElementVisible('css', 'button[type=button].link:contains(Edit)');
// Activate the machine name text field.
$page->pressButton('Edit');
$page->fillField('Machine-readable name', $name);
$page->pressButton('Save and continue');
$page->pressButton('Create group');
}
// Update title in group 1.
$page = $this->getSession()->getPage();
$page->pressButton('group_test_1_group_settings_edit');
$this->assertSession()->assertWaitOnAjaxRequest();
$page->fillField('fields[group_test_1][settings_edit_form][settings][label]', 'Test 1 - Update');
$page->pressButton('Update');
$this->assertSession()->assertWaitOnAjaxRequest();
// Update title in group 2.
$page->pressButton('group_test_2_group_settings_edit');
$this->assertSession()->assertWaitOnAjaxRequest();
$page->fillField('fields[group_test_2][settings_edit_form][settings][label]', 'Test 2 - Update');
$page->pressButton('Update');
$this->assertSession()->assertWaitOnAjaxRequest();
// Open group 1 again.
$page->pressButton('group_test_1_group_settings_edit');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertSession()->fieldValueEquals('fields[group_test_1][settings_edit_form][settings][label]', 'Test 1 - Update');
$page->pressButton('Cancel');
$this->assertSession()->assertWaitOnAjaxRequest();
$page->pressButton('Save');
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
$display = EntityFormDisplay::load("node.{$this->nodeType}.default");
$this->assertSame('Test 1 - Update', $display->getThirdPartySetting('field_group', 'group_test_1')['label']);
$this->assertSame('Test 2 - Update', $display->getThirdPartySetting('field_group', 'group_test_2')['label']);
}
}
<?php
namespace Drupal\Tests\field_group\FunctionalJavascript;
use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Core\Extension\ThemeInstallerInterface;
use Drupal\Core\Url;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\field_group\Functional\FieldGroupTestTrait;
/**
* Tests horizontal tabs labels.
*
* @group field_group
*/
class HorizontalTabsLabelsTest extends WebDriverTestBase {
use FieldGroupTestTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'block',
'field_group',
'node',
'user',
];
/**
* The themes to test with.
*
* @var string[]
*/
protected $themeList = [
'bartik',
'claro',
'classy',
'seven',
'stable',
'stable9',
'stark',
];
/**
* The themes that are shipped with block configurations.
*
* @var string[]
*/
protected $themesWithBlocks = [
'claro',
];
/**
* The webassert session.
*
* @var \Drupal\Tests\WebAssert
*/
protected $assertSession;
/**
* The page element.
*
* @var \Behat\Mink\Element\DocumentElement
*/
protected $page;
/**
* The node type used for testing.
*
* @var \Drupal\node\NodeTypeInterface
*/
protected $testNodeType;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->assertSession = $this->assertSession();
$this->page = $this->getSession()->getPage();
$this->testNodeType = $this->drupalCreateContentType([
'type' => 'test_node_bundle',
'name' => 'Test Node Type',
]);
// Add an extra field to the test content type.
$entity_type_manager = $this->container->get('entity_type.manager');
$field_storage = $entity_type_manager
->getStorage('field_storage_config')
->create([
'type' => 'string',
'field_name' => 'test_label',
'entity_type' => 'node',
]);
assert($field_storage instanceof FieldStorageConfigInterface);
$field_storage->save();
$entity_type_manager->getStorage('field_config')
->create([
'label' => 'Test label',
'field_storage' => $field_storage,
'bundle' => $this->testNodeType->id(),
])
->save();
$tab1 = [
'label' => 'Tab1',
'group_name' => 'group_tab1',
'weight' => '1',
'children' => [
0 => 'test_label',
],
'format_type' => 'tab',
'format_settings' => [
'label' => 'Tab1',
'formatter' => 'open',
],
];
$this->createGroup('node', $this->testNodeType->id(), 'form', 'default', $tab1);
$this->createGroup('node', $this->testNodeType->id(), 'view', 'default', $tab1);
$tab2 = [
'label' => 'Tab2',
'group_name' => 'group_tab2',
'weight' => '2',
'children' => [
0 => 'body',
],
'format_type' => 'tab',
'format_settings' => [
'label' => 'Tab2',
'formatter' => 'closed',
],
];
$this->createGroup('node', $this->testNodeType->id(), 'form', 'default', $tab2);
$this->createGroup('node', $this->testNodeType->id(), 'view', 'default', $tab2);
$horizontal_tabs = [
'label' => 'Horizontal tabs',
'group_name' => 'group_horizontal_tabs',
'weight' => '-5',
'children' => [
'group_tab1',
'group_tab2',
],
'format_type' => 'tabs',
'format_settings' => [
'direction' => 'horizontal',
'label' => 'Horizontal tabs',
],
];
$this->createGroup('node', $this->testNodeType->id(), 'form', 'default', $horizontal_tabs);
$this->createGroup('node', $this->testNodeType->id(), 'view', 'default', $horizontal_tabs);
$entity_type_manager->getStorage('entity_form_display')
->load(implode('.', [
'node',
$this->testNodeType->id(),
'default',
]))
->setComponent('test_label', ['weight' => '1'])
->save();
$entity_type_manager->getStorage('entity_view_display')
->load(implode('.', [
'node',
$this->testNodeType->id(),
'default',
]))
->setComponent('test_label', ['weight' => '1'])
->save();
}
/**
* Tests horizontal tabs labels.
*
* @dataProvider providerTestHorizontalTabsLabels
*/
public function testHorizontalTabsLabels(string $theme_name) {
if ($theme_name !== $this->defaultTheme) {
$theme_installer = \Drupal::service('theme_installer');
assert($theme_installer instanceof ThemeInstallerInterface);
try {
$theme_installer->install([$theme_name], TRUE);
}
catch (UnknownExtensionException $ex) {
// Themes might be missing, e.g Drupal 8.x does not have stable9 theme.
$this->pass("The $theme_name theme does not exist in the current test environment.");
return;
}
\Drupal::configFactory()
->getEditable('system.theme')
->set('default', $theme_name)
->set('admin', $theme_name)
->save();
}
if (!in_array($theme_name, $this->themesWithBlocks, TRUE)) {
$this->drupalPlaceBlock('page_title_block', [
'region' => 'content',
]);
$this->drupalPlaceBlock('local_tasks_block', [
'region' => 'content',
'weight' => 1,
]);
$this->drupalPlaceBlock('local_actions_block', [
'region' => 'content',
'weight' => 2,
]);
$this->drupalPlaceBlock('system_main_block', [
'region' => 'content',
'weight' => 3,
]);
}
$this->drupalLogin($this->rootUser);
// Actual test: check the node edit page. Tab1 and Tab2 should be present.
$this->drupalGet(Url::fromRoute('node.add', [
'node_type' => $this->testNodeType->id(),
]));
$this->assertHorizontalTabsLabels();
// Create a node.
$this->page->fillField('title[0][value]', 'Field Group Horizontal Tabs Test Node');
$this->page->fillField('Test label', 'Test label');
$this->assertNotNull($tab2 = $this->page->find('css', '.js .field-group-tabs-wrapper a[href="#edit-group-tab2"]'));
$tab2->click();
$this->assertSession->waitForElementVisible('css', '[name="body[0][value]"]');
$this->page->fillField('body[0][value]', 'Donec laoreet imperdiet.');
$this->page->findButton('edit-submit')->click();
$this->assertSession->waitForElement('css', 'html.js [data-drupal-messages]');
$status_message = $this->page->find('css', 'html.js [data-drupal-messages]');
$this->assertStringContainsString("{$this->testNodeType->label()} Field Group Horizontal Tabs Test Node has been created.", $status_message->getText());
// Check the node.
$this->drupalGet(Url::fromRoute('entity.node.canonical', [
'node' => '1',
]));
$this->assertHorizontalTabsLabels();
$this->drupalLogout();
// Retest the node with anonymous user.
$this->drupalGet(Url::fromRoute('entity.node.canonical', [
'node' => '1',
]));
$this->assertHorizontalTabsLabels();
}
/**
* Asserts the horizontal tabs labels.
*/
protected function assertHorizontalTabsLabels() {
$this->assertSession->waitForElement('css', '.js .field-group-tabs-wrapper a[href="#edit-group-tab1"]');
$this->assertSession->waitForElement('css', '.js .field-group-tabs-wrapper a[href="#edit-group-tab2"]');
$this->assertNotNull($tab1 = $this->page->find('css', '.js .field-group-tabs-wrapper a[href="#edit-group-tab1"]'));
$this->assertStringContainsString('Tab1', $tab1->getText());
$this->assertNotNull($tab2 = $this->page->find('css', '.js .field-group-tabs-wrapper a[href="#edit-group-tab2"]'));
$this->assertStringContainsString('Tab2', $tab2->getText());
}
/**
* Data provider for testHorizontalTabsLabels.
*
* @return string[][][]
* The test cases with the theme machine names.
*/
public function providerTestHorizontalTabsLabels() {
return array_reduce($this->themeList, function (array $carry, string $theme_name) {
$carry[$theme_name] = [
'theme_name' => $theme_name,
];
return $carry;
}, []);
}
}
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