70 | { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26 |
27 | Cypress.Commands.add('visitAdminPage', (page = 'index.php') => {
28 | cy.login();
29 | if (page.includes('http')) {
30 | cy.visit(page);
31 | } else {
32 | cy.visit(`/wp-admin/${page.replace(/^\/|\/$/g, '')}`);
33 | }
34 | });
35 |
36 | Cypress.Commands.add(
37 | 'clearThenType',
38 | { prevSubject: true },
39 | (subject, text, force = false) => {
40 | /* eslint-disable-next-line cypress/unsafe-to-chain-command */
41 | cy.wrap(subject).clear().type(text, { force });
42 | }
43 | );
44 |
45 | Cypress.Commands.add('wpCliEvalCustom', (command) => {
46 | const fileName = (Math.random() + 1).toString(36).substring(7);
47 |
48 | // this will be written "local" plugin directory
49 | const escapedCommand = command.replace(/^<\?php /, '');
50 | cy.writeFile(fileName, ` {
53 | const pluginName = result.stdout;
54 |
55 | // which is read from it's proper location in the plugins directory
56 | cy.exec(
57 | `npm --silent run env run tests-cli wp eval-file wp-content/plugins/${pluginName}/${fileName}` // eslint-disable-line @typescript-eslint/restrict-template-expressions
58 | ).then((commandResult) => {
59 | cy.exec(`rm ${fileName}`);
60 | cy.wrap(commandResult);
61 | });
62 | });
63 | });
64 |
65 | Cypress.Commands.add('wpCliCustom', (command, ignoreFailures = false) => {
66 | const escapedCommand = command.replace(/"/g, '\\"').replace(/^wp /, '');
67 | const options = {
68 | failOnNonZeroExit: !ignoreFailures,
69 | };
70 | cy.exec(
71 | `npm --silent run env run tests-cli wp ${escapedCommand}`,
72 | options
73 | ).then((result) => {
74 | cy.wrap(result);
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Contact Form 7 - Repeatable Fields
2 |
3 | > Adds repeatable groups of fields to Contact Form 7.
4 |
5 | [](#support-level) [](https://github.com/felipeelia/cf7-repeatable-fields) [](https://github.com/felipeelia/cf7-repeatable-fields/releases/latest)  [](https://github.com/felipeelia/cf7-repeatable-fields/blob/trunk/LICENSE.md)
6 |
7 | ## Requirements
8 |
9 | This plugin requires these software with the following versions:
10 |
11 | * [WordPress](https://wordpress.org) 6.0+
12 | * [PHP](https://php.net/) 7.2+
13 | * [Contact Form 7](https://wordpress.org/plugins/contact-form-7/) 5.7+
14 |
15 | ## Usage ##
16 |
17 | ### Form tab ###
18 | Wrap the desired fields with `[field_group your_group_id_here][/field_group]`. The shortcode accepts additional parameters, in WP shortcode format and in CF7 fields parameters format as well.
19 |
20 | Example:
21 | ```html
22 | [field_group emails id="emails-groups" tabindex:1]
23 |
24 | [radio your-radio use_label_element default:1 "radio 1" "radio 2" "radio 3"]
25 | [select* your-menu include_blank "option1" "option 2"]
26 | [checkbox* your-checkbox "check 1" "check 2"]
27 | [/field_group]
28 | ```
29 |
30 | ### Mail tab ###
31 | In the mail settings, wrap the fields with your group id. You can use the `[group_index]` tag to print the group index and an additional `__` to print a field at a specific index.
32 |
33 | Example:
34 | ```html
35 | The second email entered by the user was: [your-email__2]
36 |
37 | These were the groups:
38 | [emails]
39 | GROUP #[group_index]
40 | Checkbox: [your-checkbox]
41 | E-mail: [your-email]
42 | Radio: [your-radio]
43 | Select: [your-menu]
44 | [/emails]
45 | ```
46 |
47 | ## Check out the Wiki
48 |
49 | * [Hooks available](https://github.com/felipeelia/cf7-repeatable-fields/wiki/Hooks) - How to customize the _add_ and _remove_ buttons
50 | * [Frequently Asked Questions](https://github.com/felipeelia/cf7-repeatable-fields/wiki/Frequently-Asked-Questions)
51 |
52 | ## Contribute ##
53 | You can contribute with code, issues and ideas at the [GitHub repository](https://github.com/felipeelia/cf7-repeatable-fields).
54 |
55 | If you like the plugin, [a review](https://wordpress.org/support/plugin/cf7-repeatable-fields/reviews/#new-post) is appreciated :)
56 |
57 | ## Changelog
58 |
59 | A complete listing of all notable changes to this plugin are documented in [CHANGELOG.md](https://github.com/felipeelia/cf7-repeatable-fields/blob/trunk/CHANGELOG.md).
60 |
--------------------------------------------------------------------------------
/assets/js/scripts.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Available events:
3 | * `wpcf7-field-groups/change`: triggered by .wpcf7-field-groups elements
4 | * `wpcf7-field-groups/added`: triggered by .wpcf7-field-group-add elements
5 | * `wpcf7-field-groups/removed`: triggered by .wpcf7-field-group-remove elements
6 | */
7 |
8 | /**
9 | * Main function
10 | *
11 | * @param {Object} $ jQuery
12 | */
13 | (function ($) {
14 | 'use strict';
15 |
16 | $(function () {
17 | const $groups = $('.wpcf7-field-groups');
18 | // Only need to work if there is any group.
19 | if (!$groups.length) {
20 | return;
21 | }
22 |
23 | // Let's grab the groups models to append them when necessary.
24 | $groups.each(function () {
25 | $(this).data(
26 | 'group-model',
27 | $(this).find('.wpcf7-field-group').eq(0).clone()
28 | );
29 | });
30 |
31 | $('body').on(
32 | 'wpcf7-field-groups/change',
33 | '.wpcf7-field-groups',
34 | function () {
35 | // For each group inside this we have to adjust name parameter.
36 | const $groupsInside = $(this).find('.wpcf7-field-group');
37 | $groupsInside.each(function (index) {
38 | $(this)
39 | .find('.wpcf7-field-group-remove')
40 | .toggle(index > 0);
41 | const i = index + 1;
42 | $(this)
43 | .find('[name]')
44 | .each(function () {
45 | const $$ = $(this),
46 | $formControlWrap = $$.closest(
47 | '.wpcf7-form-control-wrap'
48 | ),
49 | name = $$.attr('name'),
50 | isArray = name.indexOf('[]') > -1,
51 | rawName = name.replace('[]', '');
52 | let newName =
53 | rawName.replace(/__[0-9]*/, '') + '__' + i;
54 |
55 | // New attribute used by CF7 to validate
56 | $formControlWrap.attr('data-name', newName);
57 |
58 | // The form control wrap class doesn't have `[]` chars...
59 | if (
60 | $formControlWrap.length &&
61 | !$formControlWrap.hasClass(newName)
62 | ) {
63 | $formControlWrap
64 | .removeClass(rawName)
65 | .addClass(newName);
66 | }
67 | // but the field can have.
68 | newName += isArray ? '[]' : '';
69 | $$.attr('name', newName);
70 | });
71 | });
72 | $(this)
73 | .find('.wpcf7-field-group-count')
74 | .val($groupsInside.length);
75 | }
76 | );
77 | // Set thing up for the first time.
78 | $groups.trigger('wpcf7-field-groups/change');
79 |
80 | // Handle the buttons action.
81 | $('body').on(
82 | 'click',
83 | '.wpcf7-field-group-add, .wpcf7-field-group-remove',
84 | function () {
85 | const $$ = $(this),
86 | $allGroups = $$.closest('.wpcf7-field-groups');
87 |
88 | if ($$.hasClass('wpcf7-field-group-add')) {
89 | const $newGroup = $allGroups
90 | .data('group-model')
91 | .clone(true);
92 | $allGroups.append($newGroup);
93 | $$.trigger('wpcf7-field-groups/added', $newGroup);
94 | } else {
95 | $$.trigger('wpcf7-field-groups/removed');
96 | $$.closest('.wpcf7-field-group').remove();
97 | }
98 | $allGroups.trigger('wpcf7-field-groups/change');
99 | return false;
100 | }
101 | );
102 |
103 | // Exclusive Checkbox
104 | $groups.on(
105 | 'click',
106 | '.wpcf7-exclusive-checkbox input:checkbox',
107 | function () {
108 | const name = $(this).attr('name');
109 | $groups
110 | .find('input:checkbox[name="' + name + '"]')
111 | .not(this)
112 | .prop('checked', false);
113 | }
114 | );
115 | });
116 | })(jQuery);
117 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at https://felipeelia.dev/contact/. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [10up's](https://github.com/10up) that is adapted from [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing and Maintaining
2 |
3 | First, thank you for taking the time to contribute!
4 |
5 | The following is a set of guidelines for contributors as well as information and instructions around our maintenance process. The two are closely tied together in terms of how we all work together and set expectations, so while you may not need to know everything in here to submit an issue or pull request, it's best to keep them in the same document.
6 |
7 | ## Ways to contribute
8 |
9 | Contributing isn't just writing code - it's anything that improves the project. All contributions are managed right here on GitHub. Here are some ways you can help:
10 |
11 | ### Reporting bugs
12 |
13 | If you're running into an issue, please take a look through [existing issues](https://github.com/felipeelia/cf7-repeatable-fields/issues) and [open a new one](https://github.com/felipeelia/cf7-repeatable-fields/issues/new) if needed. If you're able, include steps to reproduce, environment information, and screenshots/screencasts as relevant.
14 |
15 | ### Suggesting enhancements
16 |
17 | New features and enhancements are also managed via [issues](https://github.com/felipeelia/cf7-repeatable-fields/issues).
18 |
19 | ### Pull requests
20 |
21 | Pull requests represent a proposed solution to a specified problem. They should always reference an issue that describes the problem and contains discussion about the problem itself. Discussion on pull requests should be limited to the pull request itself, i.e. code review.
22 |
23 | ## Workflow
24 |
25 | The `trunk` branch is the main branch and contains all new code to be released in an upcoming version. A version released is just a tag of that branch.
26 |
27 | ## Release instructions
28 |
29 | 1. Branch: Starting from `trunk`, cut a release branch named `release/X.Y.Z` for your changes.
30 | 1. Version bump: Bump the version number in `cf7-repeatable-fields.php`, `package.json`, `package-lock.json`, `readme.txt`, and any other relevant files if it does not already reflect the version being released. In `cf7-repeatable-fields.php` update both the plugin "Version:" property and the plugin `CF7_REPEATABLE_FIELDS_VERSION` constant.
31 | 1. Changelog: Add/update the changelog in `CHANGELOG.md` and `readme.txt`, ensuring to link the [X.Y.Z] release reference in the footer of `CHANGELOG.md` (e.g., https://github.com/felipeelia/cf7-repeatable-fields/compare/X.Y.Z-1...X.Y.Z). Update the Unreleased link to https://github.com/felipeelia/cf7-repeatable-fields/compare/X.Y.Z...trunk
32 | 1. Props: Update `CREDITS.md` file with any new contributors, confirm maintainers are accurate.
33 | 1. Readme updates: Make any other readme changes as necessary. `README.md` is geared toward GitHub and `readme.txt` contains WordPress.org-specific content. The two are slightly different.
34 | 1. New files: Check to be sure any new files/paths that are unnecessary in the production version are included in `.distignore`.
35 | 1. POT file: Run `wp i18n make-pot . languages/cf7-repeatable-fields.pot` and commit the file. In case of errors, try to disable Xdebug (see [this comment](https://github.com/10up/ElasticPress/pull/3079#issuecomment-1291028290).)
36 | 1. Release date: Double check the release date in both changelog files.
37 | 1. Merge: Merge the release branch/PR into `trunk`.
38 | 1. Test: Run `git pull origin trunk` and test for functionality locally.
39 | 1. Release: Create a [new release](https://github.com/felipeelia/cf7-repeatable-fields/releases/new), naming the release and the new tag with the new version number (`X.Y.Z`). Paste the release changelog from `CHANGELOG.md` into the body of the release and include a link to the closed issues on the [milestone](https://github.com/felipeelia/cf7-repeatable-fields/milestone/#?closed=1).
40 | 1. SVN: Wait for the [GitHub Action](https://github.com/felipeelia/cf7-repeatable-fields/actions/workflows/deploy.yml) to finish deploying to the WordPress.org repository. If all goes well, users with SVN commit access for that plugin will receive an emailed diff of changes.
41 | 1. Check WordPress.org: Ensure that the changes are live on https://wordpress.org/plugins/cf7-repeatable-fields/. This may take a few minutes.
42 | 1. Close milestone: Edit the [milestone](https://github.com/felipeelia/cf7-repeatable-fields/milestone/#) with release date (in the `Due date (optional)` field) and link to GitHub release (in the `Description` field), then close the milestone.
43 | 1. Punt incomplete items: If any open issues or PRs which were milestoned for `X.Y.Z` do not make it into the release, update their milestone to `X.Y.Z+1`, `X.Y+1.0`, `X+1.0.0` or `Future Release`.
44 |
--------------------------------------------------------------------------------
/tests/e2e/integration/cf7-repeatable-fields.cy.js:
--------------------------------------------------------------------------------
1 | describe('CF7 Repeatable Fields functionality', () => {
2 | /**
3 | * Delete synonyms recreate test posts before running tests.
4 | */
5 | before(() => {
6 | cy.wpCliEvalCustom(
7 | `
8 | $content = get_posts(
9 | [
10 | 'post_type' => [ 'wpcf7_contact_form', 'page', 'email' ],
11 | 'post_status' => 'any',
12 | 'numberposts' => 999,
13 | ]
14 | );
15 | foreach( $content as $entry ) {
16 | wp_delete_post( $entry->ID, true );
17 | }`
18 | );
19 | });
20 |
21 | it('Create field and see its values in emails', () => {
22 | cy.login();
23 |
24 | // Create form
25 | cy.visitAdminPage('admin.php?page=wpcf7-new');
26 | cy.get('#wpcf7-form').as('formTextarea');
27 | cy.get('@formTextarea').clear();
28 | cy.get('@formTextarea').type(
29 | `
30 |
31 |
32 |
33 |
34 | [field_group emails id="emails-groups" tabindex:1]
35 |
36 | [radio your-radio use_label_element default:1 "radio 1" "radio 2" "radio 3"]
37 | [select* your-menu include_blank "option1" "option 2"]
38 | [checkbox* your-checkbox "check 1" "check 2"]
39 | [/field_group]
40 | [submit "Submit"]`,
41 | { delay: 0 }
42 | );
43 |
44 | cy.get('a[href="#mail-panel"]').click();
45 | cy.get('#wpcf7-mail-body').as('mailTextarea');
46 | cy.get('@mailTextarea').clear();
47 | cy.get('@mailTextarea').type(
48 | `
49 | From: [your-name] [your-email]
50 | Subject: [your-subject]
51 |
52 | Message Body:
53 | [your-message]
54 |
55 | The second email entered by the user was: [your-email__2]
56 |
57 | These were the groups:
58 | [emails]
59 | GROUP #[group_index]
60 | Checkbox: [your-checkbox]
61 | E-mail: [your-email]
62 | Radio: [your-radio]
63 | Select: [your-menu]
64 | [/emails]`,
65 | { delay: 0 }
66 | );
67 |
68 | cy.get('#publishing-action input[name="wpcf7-save"]').click();
69 | cy.get('#wpcf7-shortcode').invoke('val').as('shortcode');
70 |
71 | // Create page with the form
72 | cy.get('@shortcode').then((shortcode) => {
73 | cy.wpCliEvalCustom(
74 | `
75 | $post_id = wp_insert_post(
76 | [
77 | 'post_type' => 'page',
78 | 'post_status' => 'publish',
79 | 'post_title' => 'Contact Form',
80 | 'post_content' => '${shortcode}',
81 | ]
82 | );
83 | echo get_permalink( $post_id );
84 | `
85 | ).then((result) => {
86 | cy.visit(result.stdout);
87 | });
88 | });
89 |
90 | // Visit page and send form
91 | cy.get('input[name="your-name"]').type('Test Name');
92 | cy.get('input[name="your-email"]').type('email@domain.com');
93 | cy.get('input[name="your-subject"]').type('Subject');
94 | cy.get('textarea[name="your-message"]').type('Test message');
95 |
96 | cy.get('input[name="your-email__1"]').type('email-1@domain.com');
97 | cy.get('input[name="your-radio__1"][value="radio 1"]').check();
98 | cy.get('select[name="your-menu__1"]').select('option1');
99 | cy.get('input[name="your-checkbox__1[]"][value="check 1"]').check();
100 |
101 | cy.get('.wpcf7-field-group-add').click();
102 | cy.get('input[name="_wpcf7_groups_count[emails]__1"]')
103 | .invoke('val')
104 | .should('eq', '2');
105 | cy.get('input[name="your-email__2"]').should('exist');
106 |
107 | cy.get('.wpcf7-field-group-add').last().click();
108 | cy.get('input[name="_wpcf7_groups_count[emails]__1"]')
109 | .invoke('val')
110 | .should('eq', '3');
111 | cy.get('input[name="your-email__3"]').should('exist');
112 |
113 | cy.get('.wpcf7-field-group-remove').eq(1).click();
114 | cy.get('input[name="your-email__3"]').should('not.exist');
115 |
116 | cy.get('input[name="your-email__2"]').type('email-2@domain.com');
117 | cy.get('input[name="your-radio__2"][value="radio 2"]').check();
118 | cy.get('select[name="your-menu__2"]').select('option 2');
119 | cy.get('input[name="your-checkbox__2[]"][value="check 2"]').check();
120 |
121 | cy.get('.wpcf7-submit').click();
122 |
123 | // Check email
124 | cy.wpCliEvalCustom(
125 | `
126 | $email = get_posts(
127 | [
128 | 'post_type' => 'email',
129 | 'post_status' => 'any',
130 | 'numberposts' => 1,
131 | ]
132 | );
133 | echo $email[0]->post_content;
134 | `
135 | ).then((result) => {
136 | const postContent = result.stdout;
137 |
138 | expect(postContent).to.contain('From: Test Name email@domain.com');
139 | expect(postContent).to.contain('Subject: Subject');
140 | expect(postContent).to.contain('Test message');
141 | expect(postContent).to.contain(
142 | 'The second email entered by the user was: email-2@domain.com'
143 | );
144 | expect(postContent).to.contain(`GROUP #1
145 | Checkbox: check 1
146 | E-mail: email-1@domain.com
147 | Radio: radio 1
148 | Select: option1`);
149 | expect(postContent).to.contain(`GROUP #2
150 | Checkbox: check 2
151 | E-mail: email-2@domain.com
152 | Radio: radio 2
153 | Select: option 2`);
154 | });
155 | });
156 | });
157 |
--------------------------------------------------------------------------------
/bin/install-wp-tests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [ $# -lt 3 ]; then
4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]"
5 | exit 1
6 | fi
7 |
8 | DB_NAME=$1
9 | DB_USER=$2
10 | DB_PASS=$3
11 | DB_HOST=${4-localhost}
12 | WP_VERSION=${5-latest}
13 | SKIP_DB_CREATE=${6-false}
14 |
15 | TMPDIR=${TMPDIR-/tmp}
16 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
17 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}
18 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress}
19 |
20 | download() {
21 | if [ `which curl` ]; then
22 | curl -s "$1" > "$2";
23 | elif [ `which wget` ]; then
24 | wget -nv -O "$2" "$1"
25 | fi
26 | }
27 |
28 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
29 | WP_BRANCH=${WP_VERSION%\-*}
30 | WP_TESTS_TAG="branches/$WP_BRANCH"
31 |
32 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
33 | WP_TESTS_TAG="branches/$WP_VERSION"
34 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
35 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
36 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
37 | WP_TESTS_TAG="tags/${WP_VERSION%??}"
38 | else
39 | WP_TESTS_TAG="tags/$WP_VERSION"
40 | fi
41 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
42 | WP_TESTS_TAG="trunk"
43 | else
44 | # http serves a single offer, whereas https serves multiple. we only want one
45 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
46 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
47 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
48 | if [[ -z "$LATEST_VERSION" ]]; then
49 | echo "Latest WordPress version could not be found"
50 | exit 1
51 | fi
52 | WP_TESTS_TAG="tags/$LATEST_VERSION"
53 | fi
54 | set -ex
55 |
56 | install_wp() {
57 |
58 | if [ -d $WP_CORE_DIR ]; then
59 | return;
60 | fi
61 |
62 | mkdir -p $WP_CORE_DIR
63 |
64 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
65 | mkdir -p $TMPDIR/wordpress-trunk
66 | rm -rf $TMPDIR/wordpress-trunk/*
67 | svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress
68 | mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR
69 | else
70 | if [ $WP_VERSION == 'latest' ]; then
71 | local ARCHIVE_NAME='latest'
72 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
73 | # https serves multiple offers, whereas http serves single.
74 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json
75 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
76 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
77 | LATEST_VERSION=${WP_VERSION%??}
78 | else
79 | # otherwise, scan the releases and get the most up to date minor version of the major release
80 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'`
81 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1)
82 | fi
83 | if [[ -z "$LATEST_VERSION" ]]; then
84 | local ARCHIVE_NAME="wordpress-$WP_VERSION"
85 | else
86 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION"
87 | fi
88 | else
89 | local ARCHIVE_NAME="wordpress-$WP_VERSION"
90 | fi
91 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz
92 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
93 | fi
94 |
95 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
96 | }
97 |
98 | install_test_suite() {
99 | # portable in-place argument for both GNU sed and Mac OSX sed
100 | if [[ $(uname -s) == 'Darwin' ]]; then
101 | local ioption='-i.bak'
102 | else
103 | local ioption='-i'
104 | fi
105 |
106 | # set up testing suite if it doesn't yet exist
107 | if [ ! -d $WP_TESTS_DIR ]; then
108 | # set up testing suite
109 | mkdir -p $WP_TESTS_DIR
110 | rm -rf $WP_TESTS_DIR/{includes,data}
111 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
112 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
113 | fi
114 |
115 | if [ ! -f wp-tests-config.php ]; then
116 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
117 | # remove all forward slashes in the end
118 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
119 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
120 | sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
121 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
122 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
123 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
124 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
125 | fi
126 |
127 | }
128 |
129 | recreate_db() {
130 | shopt -s nocasematch
131 | if [[ $1 =~ ^(y|yes)$ ]]
132 | then
133 | mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA
134 | create_db
135 | echo "Recreated the database ($DB_NAME)."
136 | else
137 | echo "Leaving the existing database ($DB_NAME) in place."
138 | fi
139 | shopt -u nocasematch
140 | }
141 |
142 | create_db() {
143 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
144 | }
145 |
146 | install_db() {
147 |
148 | if [ ${SKIP_DB_CREATE} = "true" ]; then
149 | return 0
150 | fi
151 |
152 | # parse DB_HOST for port or socket references
153 | local PARTS=(${DB_HOST//\:/ })
154 | local DB_HOSTNAME=${PARTS[0]};
155 | local DB_SOCK_OR_PORT=${PARTS[1]};
156 | local EXTRA=""
157 |
158 | if ! [ -z $DB_HOSTNAME ] ; then
159 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
160 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
161 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then
162 | EXTRA=" --socket=$DB_SOCK_OR_PORT"
163 | elif ! [ -z $DB_HOSTNAME ] ; then
164 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
165 | fi
166 | fi
167 |
168 | # create database
169 | if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ]
170 | then
171 | echo "Reinstalling will delete the existing test database ($DB_NAME)"
172 | read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB
173 | recreate_db $DELETE_EXISTING_DB
174 | else
175 | create_db
176 | fi
177 | }
178 |
179 | install_wp
180 | install_test_suite
181 | install_db
182 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file, per [the Keep a Changelog standard](https://keepachangelog.com/).
4 |
5 | ## [Unreleased]
6 |
7 |
15 |
16 | ## [2.0.2] - 2024-10-22
17 |
18 | **This is a security release.** It fixes a Stored cross-site scripting (XSS) vulnerability, that allowed users with contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. Thanks to Peter Thaleikis and the Wordfence team for reaching out about it.
19 |
20 | ### Added
21 |
22 | * New `wpcf7_field_group_content` filter. Props [@Tessachu](https://github.com/Tessachu) and [@felipeelia](https://github.com/felipeelia) via [#90](https://github.com/10up/ElasticPress/pull/90).
23 | * End-to-end basic tests. Props [@felipeelia](https://github.com/felipeelia) via [#78](https://github.com/10up/ElasticPress/pull/78).
24 |
25 | ### Changed
26 |
27 | * Node version to v20. Props [@felipeelia](https://github.com/felipeelia) via [#89](https://github.com/10up/ElasticPress/pull/89).
28 |
29 | ### Security
30 |
31 | * Sanitize wrapper div attributes. Props Peter Thaleikis and [@felipeelia](https://github.com/felipeelia) via [#90](https://github.com/10up/ElasticPress/pull/90).
32 | * Bumped `postcss` from 8.4.26 to 8.4.31. Props [@dependabot](https://github.com/dependabot) via [#70](https://github.com/10up/ElasticPress/pull/70).
33 | * Updated `ws` from 8.13.0 to 8.18.0. Props [@dependabot](https://github.com/dependabot) via [#88](https://github.com/10up/ElasticPress/pull/88).
34 | * Updated `@wordpress/scripts` from 27.7.0 to 30.3.0. Props [@dependabot](https://github.com/dependabot) via [#88](https://github.com/10up/ElasticPress/pull/88).
35 | * Bumped `braces` from 3.0.2 to 3.0.3. Props [@dependabot](https://github.com/dependabot) via [#80](https://github.com/10up/ElasticPress/pull/80).
36 | * Bumped `webpack` from 5.91.0 to 5.94.0. Props [@dependabot](https://github.com/dependabot) via [#82](https://github.com/10up/ElasticPress/pull/82).
37 | * Bumped `express` from 4.18.2 to 4.19.2. Props [@dependabot](https://github.com/dependabot) via [#74](https://github.com/10up/ElasticPress/pull/74).
38 | * Bumped `follow-redirects` from 1.15.2 to 1.15.6. Props [@dependabot](https://github.com/dependabot) via [#76](https://github.com/10up/ElasticPress/pull/76).
39 | * Bumped `webpack-dev-middleware` from 5.3.3 to 5.3.4. Props [@dependabot](https://github.com/dependabot) via [#75](https://github.com/10up/ElasticPress/pull/75).
40 | * Bumped `@babel/traverse` from 7.22.8 to 7.23.2. Props [@dependabot](https://github.com/dependabot) via [#71](https://github.com/10up/ElasticPress/pull/71).
41 | * Several node packages updated. Props [@felipeelia](https://github.com/felipeelia) via [#77](https://github.com/10up/ElasticPress/pull/77).
42 |
43 | ## [2.0.1] - 2023-09-11
44 |
45 | ### Added
46 |
47 | * End-to-end tests foundation. Props [@felipeelia](https://github.com/felipeelia) via [#64](https://github.com/10up/ElasticPress/pull/64).
48 |
49 | ### Changed
50 |
51 | * Removed unnecessary files from final package. Props [@felipeelia](https://github.com/felipeelia) via [#63](https://github.com/10up/ElasticPress/pull/63).
52 |
53 | ### Fixed
54 |
55 | * Required checkbox not showing validation messages. Props [@felipeelia](https://github.com/felipeelia) via [#62](https://github.com/10up/ElasticPress/pull/62).
56 |
57 | ## [2.0.0] - 2023-07-23
58 |
59 | **Note that this version changes minimum required versions of:**
60 |
61 | * [WordPress](https://wordpress.org): 6.0+
62 | * [PHP](https://php.net/): 7.2+
63 | * [Contact Form 7](https://wordpress.org/plugins/contact-form-7/): 5.7+
64 |
65 | This release marks the (slow) resumption of this plugin development. If you want to know more about it check out [this blog post](https://felipeelia.dev/contact-form-7-repeatable-fields-2-0-0/). If you find this plugin useful, consider leaving it [a review](https://wordpress.org/support/plugin/cf7-repeatable-fields/reviews/#new-post).
66 |
67 | ### Added
68 |
69 | * Support to [wp-env](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/). See [6db4e08](https://github.com/felipeelia/cf7-repeatable-fields/commit/6db4e08).
70 | * `group_id` as a parameter to all filters. See [#51](https://github.com/felipeelia/cf7-repeatable-fields/pull/51).
71 | * Very basic unit testing. See [#52](https://github.com/felipeelia/cf7-repeatable-fields/pull/52).
72 |
73 | ### Changed
74 |
75 | * Linting tools and script build process. See [6db4e08](https://github.com/felipeelia/cf7-repeatable-fields/commit/6db4e08).
76 | * Docs were migrated to [GitHub wiki](https://github.com/felipeelia/cf7-repeatable-fields/wiki). See [14fdd49](https://github.com/felipeelia/cf7-repeatable-fields/commit/14fdd49).
77 |
78 | ### Fixed
79 |
80 | * Validation problem with Contact Form 5.7+. Props [@sfdeveloper](https://profiles.wordpress.org/sfdeveloper/) via [this comment](https://wordpress.org/support/topic/compatibility-issues-with-cf7-5-7/#post-16588238) - added in [6db4e08](https://github.com/felipeelia/cf7-repeatable-fields/commit/6db4e08).
81 |
82 | ## [1.1.3] - 2019-12-11
83 |
84 | * Update WP `Tested up to` field
85 | * Apply WP Coding Standards
86 | * Fix a small sanitization problem
87 |
88 | ## [1.1.2] - 2019-10-10
89 |
90 | * Fix Exclusive Checkboxes
91 |
92 | ## [1.1.1] - 2019-09-04
93 |
94 | * Add compatibility to formatted dates (`[_format_{field name} "{date format}"]`)
95 | * DEV: Copy data and events while cloning a new group (JS)
96 | * DEV: Pass `$new_group` as an extra param for the `wpcf7-field-groups/added` event.
97 | * DEV: Apply some WPCS rules and add a CF7_REPEATABLE_FIELDS_VERSION const (may affect JS cache)
98 |
99 | ## [1.1] - 2018-06-14
100 |
101 | * Replace groups in mail 2 field
102 |
103 | ## [1.0.2] - 2018/03/29
104 |
105 | * Fix repeated tags in mail body
106 |
107 | ## [1.0.1] - 2018/03/20
108 |
109 | * Fix the `wpcf7_field_group_remove_button_atts` filter name. Props to @asilvestre87
110 |
111 | ## [1.0.0] - 2018/03/19
112 |
113 | * Initial release
114 |
115 | [Unreleased]: https://github.com/felipeelia/cf7-repeatable-fields/compare/2.0.2...trunk
116 | [2.0.2]: https://github.com/felipeelia/cf7-repeatable-fields/compare/2.0.1...2.0.2
117 | [2.0.1]: https://github.com/felipeelia/cf7-repeatable-fields/compare/2.0.0...2.0.1
118 | [2.0.0]: https://github.com/felipeelia/cf7-repeatable-fields/compare/1.1.3...2.0.0
119 | [1.1.3]: https://github.com/felipeelia/cf7-repeatable-fields/compare/1.1.2...1.1.3
120 | [1.1.2]: https://github.com/felipeelia/cf7-repeatable-fields/compare/1.1.1...1.1.2
121 | [1.1.1]: https://github.com/felipeelia/cf7-repeatable-fields/compare/1.1...1.1.1
122 | [1.1]: https://github.com/felipeelia/cf7-repeatable-fields/compare/1.0.2...1.1
123 | [1.0.2]: https://github.com/felipeelia/cf7-repeatable-fields/compare/1.0.1...1.0.2
124 | [1.0.1]: https://github.com/felipeelia/cf7-repeatable-fields/compare/1.0.0...1.0.1
125 | [1.0.0]: https://github.com/felipeelia/cf7-repeatable-fields/releases/tag/1.0.0
126 |
--------------------------------------------------------------------------------
/readme.txt:
--------------------------------------------------------------------------------
1 | === Contact Form 7 - Repeatable Fields ===
2 | Contributors: felipeelia
3 | Donate link: https://felipeelia.dev/contact-form-7-repeatable-fields/
4 | Tags: contact form 7, cf7, repeater, repeatable
5 | Tested up to: 6.6
6 | Stable tag: 2.0.2
7 | License: GPLv2 or later
8 | License URI: http://www.gnu.org/licenses/gpl-2.0.html
9 |
10 | Adds repeatable groups of fields to Contact Form 7.
11 |
12 | == Description ==
13 | This plugin adds repeatable groups of fields to Contact Form 7.
14 |
15 | **NOTE:** Tested with Contact Form 7 5.7.7.
16 |
17 | == Usage ==
18 |
19 | = Form tab =
20 | Wrap the desired fields with `[field_group your_group_id_here][/field_group]`. The shortcode accepts additional parameters, in WP shortcode format and in CF7 fields parameters format as well.
21 |
22 | Example:
23 | ~~~
24 | [field_group emails id="emails-groups" tabindex:1]
25 |
26 | [radio your-radio use_label_element default:1 "radio 1" "radio 2" "radio 3"]
27 | [select* your-menu include_blank "option1" "option 2"]
28 | [checkbox* your-checkbox "check 1" "check 2"]
29 | [/field_group]
30 | ~~~
31 |
32 | = Mail tab =
33 | In the mail settings, wrap the fields with your group id. You can use the `[group_index]` tag to print the group index and an additional `__` to print a field at a specific index.
34 |
35 | Example:
36 | ~~~
37 | The second email entered by the user was: [your-email__2]
38 |
39 | These were the groups:
40 | [emails]
41 | GROUP #[group_index]
42 | Checkbox: [your-checkbox]
43 | E-mail: [your-email]
44 | Radio: [your-radio]
45 | Select: [your-menu]
46 | [/emails]
47 | ~~~
48 |
49 | == Check out the Wiki ==
50 |
51 | * [Hooks available](https://github.com/felipeelia/cf7-repeatable-fields/wiki/Hooks) - How to customize the _add_ and _remove_ buttons
52 | * [Frequently Asked Questions](https://github.com/felipeelia/cf7-repeatable-fields/wiki/Frequently-Asked-Questions)
53 |
54 | == Contribute ==
55 | You can contribute with code, issues and ideas at the [GitHub repository](https://github.com/felipeelia/cf7-repeatable-fields).
56 |
57 | If you like the plugin, [a review](https://wordpress.org/support/plugin/cf7-repeatable-fields/reviews/#new-post) is appreciated :)
58 |
59 | == Frequently Asked Questions ==
60 |
61 | = I have a problem with the plugin. Where can I get help? =
62 |
63 | If you have identified a bug or would like to suggest an enhancement, please refer to our [GitHub repo](https://github.com/felipeelia/cf7-repeatable-fields). I do not provide support here at WordPress.org forums.
64 |
65 | = My question is not listed here. Can I search somewhere else? =
66 |
67 | Yes! Give a look at the [Frequently Asked Questions](https://github.com/felipeelia/cf7-repeatable-fields/wiki/Frequently-Asked-Questions) section of our wiki.
68 |
69 | == Changelog ==
70 |
71 | = 2.0.2 - 2024-10-22 =
72 |
73 | **This is a security release.** It fixes a Stored cross-site scripting (XSS) vulnerability, that allowed users with contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. Thanks to Peter Thaleikis and the Wordfence team for reaching out about it.
74 |
75 | __Added:__
76 |
77 | * New `wpcf7_field_group_content` filter. Props [@Tessachu](https://github.com/Tessachu) and [@felipeelia](https://github.com/felipeelia).
78 | * End-to-end basic tests. Props [@felipeelia](https://github.com/felipeelia).
79 |
80 | __Changed:__
81 |
82 | * Node version to v20. Props [@felipeelia](https://github.com/felipeelia).
83 |
84 | __Security:__
85 |
86 | * Sanitize wrapper div attributes. Props Peter Thaleikis and [@felipeelia](https://github.com/felipeelia).
87 | * Bumped `postcss` from 8.4.26 to 8.4.31. Props [@dependabot](https://github.com/dependabot).
88 | * Updated `ws` from 8.13.0 to 8.18.0. Props [@dependabot](https://github.com/dependabot).
89 | * Updated `@wordpress/scripts` from 27.7.0 to 30.3.0. Props [@dependabot](https://github.com/dependabot).
90 | * Bumped `braces` from 3.0.2 to 3.0.3. Props [@dependabot](https://github.com/dependabot).
91 | * Bumped `webpack` from 5.91.0 to 5.94.0. Props [@dependabot](https://github.com/dependabot).
92 | * Bumped `express` from 4.18.2 to 4.19.2. Props [@dependabot](https://github.com/dependabot).
93 | * Bumped `follow-redirects` from 1.15.2 to 1.15.6. Props [@dependabot](https://github.com/dependabot).
94 | * Bumped `webpack-dev-middleware` from 5.3.3 to 5.3.4. Props [@dependabot](https://github.com/dependabot).
95 | * Bumped `@babel/traverse` from 7.22.8 to 7.23.2. Props [@dependabot](https://github.com/dependabot).
96 | * Several node packages updated. Props [@felipeelia](https://github.com/felipeelia).
97 |
98 | = 2.0.1 - 2023-09-11 =
99 |
100 | __Added:__
101 |
102 | * End-to-end tests foundation.
103 |
104 | __Changed:__
105 |
106 | * Removed unnecessary files from final package.
107 |
108 | __Fixed:__
109 |
110 | * Required checkbox not showing validation messages.
111 |
112 | = 2.0.0 - 2023-07-23 =
113 |
114 | **Note that this version changes minimum required versions of:**
115 |
116 | * [WordPress](https://wordpress.org): 6.0+
117 | * [PHP](https://php.net/): 7.2+
118 | * [Contact Form 7](https://wordpress.org/plugins/contact-form-7/): 5.7+
119 |
120 | This release marks the (slow) resumption of this plugin development. If you want to know more about it check out [this blog post](https://felipeelia.dev/contact-form-7-repeatable-fields-2-0-0/). If you find this plugin useful, consider leaving it [a review](https://wordpress.org/support/plugin/cf7-repeatable-fields/reviews/#new-post).
121 |
122 | __Added:__
123 |
124 | * Support to [wp-env](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/).
125 | * `group_id` as a parameter to all filters.
126 | * Very basic unit testing.
127 |
128 | __Changed:__
129 |
130 | * Linting tools and script build process.
131 | * Docs were migrated to [GitHub wiki](https://github.com/felipeelia/cf7-repeatable-fields/wiki).
132 |
133 | __Fixed:__
134 |
135 | * Validation problem with Contact Form 5.7+. Props [@sfdeveloper](https://profiles.wordpress.org/sfdeveloper/).
136 |
137 | = 1.1.3 - 2019-12-11 =
138 |
139 | * Update WP `Tested up to` field
140 | * Apply WP Coding Standards
141 | * Fix a small sanitization problem
142 |
143 | = 1.1.2 - 2019-10-10 =
144 |
145 | * Fix Exclusive Checkboxes
146 |
147 | = 1.1.1 - 2019-09-04 =
148 |
149 | * Add compatibility to formatted dates (`[_format_{field name} "{date format}"]`)
150 | * DEV: Copy data and events while cloning a new group (JS)
151 | * DEV: Pass `$new_group` as an extra param for the `wpcf7-field-groups/added` event.
152 | * DEV: Apply some WPCS rules and add a CF7_REPEATABLE_FIELDS_VERSION const (may affect JS cache)
153 |
154 | = 1.1 - 2018-06-14 =
155 |
156 | * Replace groups in mail 2 field
157 |
158 | = 1.0.2 - 2018/03/29 =
159 |
160 | * Fix repeated tags in mail body
161 |
162 | = 1.0.1 - 2018/03/20 =
163 |
164 | * Fix the `wpcf7_field_group_remove_button_atts` filter name. Props to @asilvestre87
165 |
166 | = 1.0.0 - 2018/03/19 =
167 |
168 | * Initial release
169 |
170 | == Upgrade Notice ==
171 |
172 | = 2.0.0 =
173 | This version changes the minimum requirements of the plugin: PHP 7.2+, WordPress 6.0+, and Contact Form 7 5.7+.
174 |
--------------------------------------------------------------------------------
/class-cf7-repeatable-fields.php:
--------------------------------------------------------------------------------
1 | shortcode, array( $this, 'shortcode_render' ) );
42 | add_action( 'wpcf7_contact_form', array( $this, 'wpcf7_contact_form' ) );
43 | add_action( 'wpcf7_enqueue_scripts', array( $this, 'wpcf7_enqueue_scripts' ) );
44 | }
45 |
46 | /**
47 | * The group shortcode. Generate the `
` element with fields and
48 | * set `$this->tags`.
49 | *
50 | * @param array $atts Group attributes. Should have a value wo attribute to be used as the group ID.
51 | * @param string $content Everything inside the shortcode. Hopefully CF7 fields (raw tags).
52 | * @return string $content with the add and remove buttons wrapped by a `div`.
53 | */
54 | public function shortcode_render( $atts, $content ) {
55 | // Respect classes sent by user, but add the necessary class for js.
56 | $atts = ( empty( $atts ) ) ? array() : $atts;
57 | $atts['class'] = ( isset( $atts['class'] ) ) ? $atts['class'] : '';
58 | $atts['class'] = 'wpcf7-field-groups ' . $atts['class'];
59 |
60 | $group_id = '';
61 | $atts = array_map(
62 | function( $att, $value ) use ( &$group_id ) {
63 | // WordPress sets numeric atts if the `attr="value"` format isn't used.
64 | if ( is_int( $att ) ) {
65 | if ( false === strpos( $value, ':' ) ) {
66 | $att = 'data-wpcf7-group-id';
67 | $group_id = $value;
68 | } else {
69 | // User can send attributes in the same format of CF7, i.e., `attr:value`.
70 | list( $att, $value ) = explode( ':', $value );
71 | }
72 | }
73 | return sprintf( '%s="%s"', $att, esc_attr( $value ) );
74 | },
75 | array_keys( $atts ),
76 | $atts
77 | );
78 | // Abort if there is no group id.
79 | if ( empty( $group_id ) ) {
80 | return sprintf(
81 | /* translators: Format to use surrounded by code tag */
82 | '
' . __( 'You need to set an ID to this group. Use %s format.', 'cf7-repeatable-fields' ) . '
',
83 | "[{$this->shortcode} your_custom_id]"
84 | );
85 | }
86 |
87 | $form_tags_manager = WPCF7_FormTagsManager::get_instance();
88 | $this->groups[ $group_id ] = array(
89 | 'tags' => $form_tags_manager->scan( $content ),
90 | 'raw' => $content,
91 | );
92 |
93 | // Add and remove group buttons. TODO: make this available from form edit screen.
94 | /**
95 | * Filters the add button attributes. Additional classes and text, so far.
96 | *
97 | * @param array $add_button_atts Array of strings with `group_id`, `additional_classes` and
98 | * `text` as indexes.
99 | */
100 | $add_button_atts = apply_filters(
101 | 'wpcf7_field_group_add_button_atts',
102 | array(
103 | 'group_id' => $group_id,
104 | 'additional_classes' => '',
105 | 'text' => '+',
106 | )
107 | );
108 | /**
109 | * Filters the whole add group button. This way developers can wrap it with another element.
110 | *
111 | * @param string $button_html The HTML of the add button.
112 | * @param string $group_id Current group ID.
113 | */
114 | $add_button = apply_filters(
115 | 'wpcf7_field_group_add_button',
116 | "',
119 | $group_id
120 | );
121 |
122 | /**
123 | * Filters the remove button attributes. Additional classes and text, so far.
124 | *
125 | * @param array $remove_button_atts Array of strings with `group_id`, `additional_classes` and
126 | * `text` as indexes.
127 | */
128 | $remove_button_atts = apply_filters(
129 | 'wpcf7_field_group_remove_button_atts',
130 | array(
131 | 'group_id' => $group_id,
132 | 'additional_classes' => '',
133 | 'text' => '-',
134 | )
135 | );
136 | /**
137 | * Filters the whole remove group button. This way developers can wrap it with another element.
138 | *
139 | * @param string $button_html The HTML of the remove button.
140 | * @param string $group_id Current group ID.
141 | */
142 | $remove_button = apply_filters(
143 | 'wpcf7_field_group_remove_button',
144 | "',
147 | $group_id
148 | );
149 |
150 | // Remove any attribute that is not allowed by CF7.
151 | $open_tag = wpcf7_kses( '