├── js ├── gf-repeater.min.js ├── gf-repeater-admin.min.js ├── gf-repeater-admin.js ├── jquery.postcapture.min.js └── gf-repeater.js ├── .gitignore ├── assets ├── banner-772x250.png ├── icon-128x128.png ├── icon-256x256.png ├── screenshot-1.png ├── screenshot-2.png ├── screenshot-3.png ├── screenshot-4.png └── banner-1544x500.png ├── languages ├── gravityformsrepeater-sv_SE.mo ├── gravityformsrepeater.pot └── gravityformsrepeater-sv_SE.po ├── CONTRIBUTING.md ├── TODO.md ├── composer.json ├── css ├── gf-repeater.css └── gf-repeater.scss ├── repeater.php ├── class-gf-field-repeater-end.php ├── changelog.txt ├── readme.txt ├── README.md └── class-gf-field-repeater.php /js/gf-repeater.min.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /assets/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kodie/gravityforms-repeater/HEAD/assets/banner-772x250.png -------------------------------------------------------------------------------- /assets/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kodie/gravityforms-repeater/HEAD/assets/icon-128x128.png -------------------------------------------------------------------------------- /assets/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kodie/gravityforms-repeater/HEAD/assets/icon-256x256.png -------------------------------------------------------------------------------- /assets/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kodie/gravityforms-repeater/HEAD/assets/screenshot-1.png -------------------------------------------------------------------------------- /assets/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kodie/gravityforms-repeater/HEAD/assets/screenshot-2.png -------------------------------------------------------------------------------- /assets/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kodie/gravityforms-repeater/HEAD/assets/screenshot-3.png -------------------------------------------------------------------------------- /assets/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kodie/gravityforms-repeater/HEAD/assets/screenshot-4.png -------------------------------------------------------------------------------- /assets/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kodie/gravityforms-repeater/HEAD/assets/banner-1544x500.png -------------------------------------------------------------------------------- /languages/gravityformsrepeater-sv_SE.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kodie/gravityforms-repeater/HEAD/languages/gravityformsrepeater-sv_SE.mo -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | * Work in the [Development Branch](https://github.com/kodie/gravityforms-repeater/tree/development). 4 | * Submit a [Pull Request](https://github.com/kodie/gravityforms-repeater/compare). 5 | * ??? 6 | * Profit! 7 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # The To-Do List 2 | 3 | ### Priority 4 | * Add support for ajax enabled forms. 5 | * Add support for more, if not all, field types. 6 | * Make form data exporting better. 7 | * Find another way to store repeated field data during validation so that postCapture dependency is not necessary. 8 | * Make fields use their own functions for validation and possibly for data fetching/formatting. 9 | * Allow zero repeat count. 10 | 11 | ### Possibly 12 | * Localize repeater php data so that sending it via a serialized array inside of a hidden field is not necessary. 13 | * ^ After that, remove the need to loop through fields to get repeater children via javascript. 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kodie/repeater-add-on-for-gravity-forms", 3 | "type": "wordpress-plugin", 4 | "license": "MIT", 5 | "description": "A Gravity Forms add-on that allows specified groups of fields to be repeated by the user.", 6 | "homepage": "https://github.com/kodie/gravityforms-repeater", 7 | "authors": [ 8 | { 9 | "name": "Kodie Grantham", 10 | "email": "kodie.grantham@gmail.com", 11 | "homepage": "http://kodieg.com" 12 | } 13 | ], 14 | "keywords": [ 15 | "wordpress" 16 | ], 17 | "support": { 18 | "issues": "https://github.com/kodie/gravityforms-repeater/issues", 19 | "forum": "https://wordpress.org/support/plugin/repeater-add-on-for-gravity-forms" 20 | }, 21 | "require": { 22 | "php": ">=5.3.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /css/gf-repeater.css: -------------------------------------------------------------------------------- 1 | h2.gf_repeater_title{font-size:1.3em;font-weight:700;line-height:1.25;margin:25px 0 0 0;padding:0}.gsection_description.gf_repeater_description{font-size:.775em}.gf_repeater_add>*,.gf_repeater_remove>*{cursor:pointer;margin:3px}.gf_repeater_add .gf_repeater_add_default,.gf_repeater_remove .gf_repeater_remove_default{background-size:1em 1em;height:1em;width:1em;opacity:0.5}.gf_repeater_add .gf_repeater_add_default:hover,.gf_repeater_remove .gf_repeater_remove_default:hover{opacity:1}.gf_repeater_add .gf_repeater_add_default{background-image:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxnIGlkPSJpY29tb29uLWlnbm9yZSI+PC9nPjxwYXRoIGQ9Ik0yNTYgNTEyYy0xNDEuMzc1IDAtMjU2LTExNC42MDktMjU2LTI1NnMxMTQuNjI1LTI1NiAyNTYtMjU2YzE0MS4zOTEgMCAyNTYgMTE0LjYwOSAyNTYgMjU2cy0xMTQuNjA5IDI1Ni0yNTYgMjU2ek0yNTYgNjRjLTEwNi4wMzEgMC0xOTIgODUuOTY5LTE5MiAxOTJzODUuOTY5IDE5MiAxOTIgMTkyYzEwNi4wNDcgMCAxOTItODUuOTY5IDE5Mi0xOTJzLTg1Ljk1My0xOTItMTkyLTE5MnpNMjg4IDM4NGgtNjR2LTk2aC05NnYtNjRoOTZ2LTk2aDY0djk2aDk2djY0aC05NnY5NnoiPjwvcGF0aD48L3N2Zz4=")}.gf_repeater_remove .gf_repeater_remove_default{background-image:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxnIGlkPSJpY29tb29uLWlnbm9yZSI+PC9nPjxwYXRoIGQ9Ik0yNTYgMGMtMTQxLjM3NSAwLTI1NiAxMTQuNjI1LTI1NiAyNTYgMCAxNDEuMzkxIDExNC42MjUgMjU2IDI1NiAyNTYgMTQxLjM5MSAwIDI1Ni0xMTQuNjA5IDI1Ni0yNTYgMC0xNDEuMzc1LTExNC42MDktMjU2LTI1Ni0yNTZ6TTI1NiA0NDhjLTEwNi4wMzEgMC0xOTItODUuOTY5LTE5Mi0xOTJzODUuOTY5LTE5MiAxOTItMTkyYzEwNi4wNDcgMCAxOTIgODUuOTY5IDE5MiAxOTJzLTg1Ljk1MyAxOTItMTkyIDE5MnpNMTI4IDI4OGgyNTZ2LTY0aC0yNTZ2NjR6Ij48L3BhdGg+PC9zdmc+")}.gf_repeater_hide_add .gf_repeater_add,.gf_repeater_hide_remove .gf_repeater_remove{display:none !important;visibility:hidden !important} 2 | -------------------------------------------------------------------------------- /css/gf-repeater.scss: -------------------------------------------------------------------------------- 1 | h2.gf_repeater_title { 2 | font-size: 1.3em; 3 | font-weight: 700; 4 | line-height: 1.25; 5 | margin: 25px 0 0 0; 6 | padding: 0; 7 | } 8 | .gsection_description.gf_repeater_description { 9 | font-size: .775em; 10 | } 11 | %repeater_end { 12 | > * { 13 | cursor: pointer; 14 | margin: 3px; 15 | } 16 | } 17 | %repeater_end_default { 18 | background-size: 1em 1em; 19 | height: 1em; 20 | width: 1em; 21 | opacity: 0.5; 22 | &:hover { opacity: 1; } 23 | } 24 | .gf_repeater_add { 25 | @extend %repeater_end; 26 | .gf_repeater_add_default { 27 | @extend %repeater_end_default; 28 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxnIGlkPSJpY29tb29uLWlnbm9yZSI+PC9nPjxwYXRoIGQ9Ik0yNTYgNTEyYy0xNDEuMzc1IDAtMjU2LTExNC42MDktMjU2LTI1NnMxMTQuNjI1LTI1NiAyNTYtMjU2YzE0MS4zOTEgMCAyNTYgMTE0LjYwOSAyNTYgMjU2cy0xMTQuNjA5IDI1Ni0yNTYgMjU2ek0yNTYgNjRjLTEwNi4wMzEgMC0xOTIgODUuOTY5LTE5MiAxOTJzODUuOTY5IDE5MiAxOTIgMTkyYzEwNi4wNDcgMCAxOTItODUuOTY5IDE5Mi0xOTJzLTg1Ljk1My0xOTItMTkyLTE5MnpNMjg4IDM4NGgtNjR2LTk2aC05NnYtNjRoOTZ2LTk2aDY0djk2aDk2djY0aC05NnY5NnoiPjwvcGF0aD48L3N2Zz4='); 29 | } 30 | } 31 | .gf_repeater_remove { 32 | @extend %repeater_end; 33 | .gf_repeater_remove_default { 34 | @extend %repeater_end_default; 35 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxnIGlkPSJpY29tb29uLWlnbm9yZSI+PC9nPjxwYXRoIGQ9Ik0yNTYgMGMtMTQxLjM3NSAwLTI1NiAxMTQuNjI1LTI1NiAyNTYgMCAxNDEuMzkxIDExNC42MjUgMjU2IDI1NiAyNTYgMTQxLjM5MSAwIDI1Ni0xMTQuNjA5IDI1Ni0yNTYgMC0xNDEuMzc1LTExNC42MDktMjU2LTI1Ni0yNTZ6TTI1NiA0NDhjLTEwNi4wMzEgMC0xOTItODUuOTY5LTE5Mi0xOTJzODUuOTY5LTE5MiAxOTItMTkyYzEwNi4wNDcgMCAxOTIgODUuOTY5IDE5MiAxOTJzLTg1Ljk1MyAxOTItMTkyIDE5MnpNMTI4IDI4OGgyNTZ2LTY0aC0yNTZ2NjR6Ij48L3BhdGg+PC9zdmc+'); 36 | } 37 | } 38 | .gf_repeater_hide_add .gf_repeater_add, .gf_repeater_hide_remove .gf_repeater_remove { 39 | display: none !important; 40 | visibility: hidden !important; 41 | } -------------------------------------------------------------------------------- /repeater.php: -------------------------------------------------------------------------------- 1 | Visit plugin site', 27 | 'GitHub' 28 | ); 29 | $links = array_merge($links, $new_links); 30 | } 31 | return $links; 32 | } 33 | 34 | if (class_exists("GFForms")) { 35 | GFForms::include_addon_framework(); 36 | 37 | class GFRepeater extends GFAddOn { 38 | protected $_version = GF_REPEATER_VERSION; 39 | protected $_min_gravityforms_version = "1.9"; 40 | protected $_slug = "gravityformsrepeater"; 41 | protected $_path = GF_REPEATER_PATH; 42 | protected $_full_path = __FILE__; 43 | protected $_title = "Gravity Forms Repeater Add-On"; 44 | protected $_short_title = "Repeater Add-On"; 45 | 46 | public function scripts() { 47 | $scripts = array( 48 | array( 49 | "handle" => "gf_repeater_js_admin", 50 | "src" => $this->get_base_url() . "/js/gf-repeater-admin.js", 51 | "version" => $this->_version, 52 | "deps" => array('jquery'), 53 | "in_footer" => false, 54 | "callback" => array($this, 'localize_scripts'), 55 | "strings" => array('page' => rgget('page'), 'debug' => GF_REPEATER_DEBUG), 56 | "enqueue" => array( 57 | array( 58 | "admin_page" => array('form_editor', 'entry_view', 'entry_detail') 59 | ) 60 | ) 61 | ) 62 | ); 63 | return array_merge(parent::scripts(), $scripts); 64 | } 65 | 66 | public function init_admin() { 67 | parent::init_admin(); 68 | GF_Field_Repeater::init_admin(); 69 | GF_Field_Repeater_End::init_admin(); 70 | } 71 | 72 | public function init_frontend() { 73 | parent::init_frontend(); 74 | GF_Field_Repeater::init_frontend(); 75 | } 76 | 77 | public function upgrade($previous_version) { 78 | if (version_compare($previous_version, '1.0.5') == -1) { 79 | $forms = GFAPI::get_forms(true); 80 | 81 | foreach ($forms as $form) { 82 | $entries = GFAPI::get_entries($form['id']); 83 | $fields = GFAPI::get_fields_by_type($form, 'repeater'); 84 | 85 | foreach ($entries as $entry) { 86 | foreach ($fields as $field) { 87 | if (array_key_exists($field['id'], $entry)) { 88 | $dataArray = GFFormsModel::unserialize($entry[$field['id']]); 89 | $dataUpdated = false; 90 | 91 | if (!is_array($dataArray)) { continue; } 92 | 93 | foreach ($dataArray as $repeaterChildId=>$repeaterChild) { 94 | foreach ($repeaterChild as $repeatedFieldId=>$repeatedField) { 95 | if (!is_array($repeatedField)) { 96 | if ($repeatedField !== '[gfRepeater-section]') { 97 | $dataUpdated = true; 98 | $dataArray[$repeaterChildId][$repeatedFieldId] = Array($repeatedField); 99 | } 100 | } elseif (reset($repeatedField) == '[gfRepeater-section]') { 101 | $dataUpdated = true; 102 | $dataArray[$repeaterChildId][$repeatedFieldId] = reset($repeatedField); 103 | } 104 | } 105 | } 106 | 107 | if ($dataUpdated) { GFAPI::update_entry_field($entry['id'], $field['id'], maybe_serialize($dataArray)); } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | new GFRepeater(); 116 | 117 | require_once('class-gf-field-repeater.php'); 118 | require_once('class-gf-field-repeater-end.php'); 119 | } 120 | -------------------------------------------------------------------------------- /languages/gravityformsrepeater.pot: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: \n" 4 | "POT-Creation-Date: 2017-01-13 11:19+0100\n" 5 | "PO-Revision-Date: 2017-01-13 11:27+0100\n" 6 | "Last-Translator: \n" 7 | "Language-Team: \n" 8 | "Language: sv_SE\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "X-Generator: Poedit 1.8.10\n" 13 | "X-Poedit-KeywordsList: __;_e;__ngettext:1,2;_n:1,2;__ngettext_noop:1,2;" 14 | "_n_noop:1,2;_c,_nc:4c,1,2;_x:1,2c;_nx:4c,1,2;_nx_noop:4c,1,2;_ex:1,2c;" 15 | "esc_attr__;esc_attr_e;esc_attr_x:1,2c;esc_html__;esc_html_e;esc_html_x:1,2c\n" 16 | "X-Poedit-Basepath: .\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 18 | "X-Poedit-SearchPath-0: .\n" 19 | "X-Poedit-SearchPath-1: ..\n" 20 | 21 | #: ../class-gf-field-repeater-end.php:79 22 | msgid "" 23 | "The HTML to replace the default add button. This HTML will be placed inside " 24 | "of a span tag with the class 'gf_repeater_add'." 25 | msgstr "" 26 | "HTML:en som ska ersätta standard \"lägg till\"-knappen. Denna HTML kommer " 27 | "att placeras inuti en span-tagg med klassen 'gf_repeater_add'." 28 | 29 | #: ../class-gf-field-repeater-end.php:80 30 | msgid "" 31 | "The HTML to replace the default remove button. This HTML will be placed " 32 | "inside of a span tag with the class 'gf_repeater_remove'." 33 | msgstr "" 34 | "HTML:en som ska ersätta standard \"ta bort\"-knappen. Denna HTML kommer att " 35 | "placeras inuti en span-tagg med klassen 'gf_repeater_remove'." 36 | 37 | #: ../class-gf-field-repeater-end.php:81 38 | msgid "" 39 | "If enabled, no add and remove buttons will be displayed. This is useful if " 40 | "you plan on using custom javascript to trigger the repeater." 41 | msgstr "" 42 | "Om aktiverad så kommer inga \"lägg till\"- eller \"ta bort\"-knappar att " 43 | "visas. Detta är användbart om du planerar på att använda eget javascript för " 44 | "att trigga repeatern." 45 | 46 | #: ../class-gf-field-repeater.php:40 47 | msgid "This field is required." 48 | msgstr "Fältet är obligatoriskt." 49 | 50 | #: ../class-gf-field-repeater.php:160 51 | msgid "" 52 | "The number of times the repeater will be repeated when the form is rendered. " 53 | "Leaving this field blank or setting it to a number higher than the maximum " 54 | "number is the same as setting it to 1." 55 | msgstr "" 56 | "Antalet gånger denna repeater ska repeatas när formuläret renderas. Att " 57 | "lämna fältet tomt eller sätta ett nummer högre än det maximala numret är " 58 | "detsamma som att sätta det till 1." 59 | 60 | #: ../class-gf-field-repeater.php:161 61 | msgid "" 62 | "The minimum number of times the repeater is allowed to be repeated. Leaving " 63 | "this field blank or setting it to a number higher than the maximum field is " 64 | "the same as setting it to 1." 65 | msgstr "" 66 | "Minst antal gånger den här repeatern får repeatas. Att lämna fältet tomt " 67 | "eller sätta ett nummer högre än det maximala numret är detsamma som att " 68 | "sätta det till 1." 69 | 70 | #: ../class-gf-field-repeater.php:162 71 | msgid "" 72 | "The maximum number of times the repeater is allowed to be repeated. Leaving " 73 | "this field blank or setting it to a number lower than the minimum field is " 74 | "the same as setting it to unlimited." 75 | msgstr "" 76 | "Maximalt antal gånger den här repeatern får repeatas. Att lämna fältet tomt " 77 | "eller sätta ett nummer lägre än det minimala numret är detsamma som att " 78 | "sätta det till oändligt." 79 | 80 | #: ../class-gf-field-repeater.php:163 81 | msgid "" 82 | "A JavaScript object to be passed for animation settings. For advanced users " 83 | "only. Do not include initial brackets." 84 | msgstr "" 85 | "Ett JavaScript-object som ska skickas in för animationsinställningar. Endast " 86 | "för avancerade användare. Inkludera inte initiala brackets." 87 | 88 | #: ../class-gf-field-repeater.php:164 89 | msgid "" 90 | "If this is checked, the repeater label and description will not be shown to " 91 | "users on the form." 92 | msgstr "" 93 | "Om den här är i-kryssad kommer repeaterns label och beskriving inte visas " 94 | "för användare på formuläret." 95 | 96 | #: ../class-gf-field-repeater.php:186 97 | #, php-format 98 | msgid "A minimum number of %s is required." 99 | msgstr "En siffra som är minst %s krävs." 100 | 101 | #: ../class-gf-field-repeater.php:192 102 | #, php-format 103 | msgid "A maximum number of %s is allowed." 104 | msgstr "En siffra som är max %s krävs." 105 | 106 | #: ../class-gf-field-repeater.php:249 107 | msgid "A required field was left blank." 108 | msgstr "Ett obligatoriskt fält har lämnats tomt." 109 | -------------------------------------------------------------------------------- /languages/gravityformsrepeater-sv_SE.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: \n" 4 | "POT-Creation-Date: 2017-01-13 11:19+0100\n" 5 | "PO-Revision-Date: 2017-01-13 11:27+0100\n" 6 | "Last-Translator: \n" 7 | "Language-Team: \n" 8 | "Language: sv_SE\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "X-Generator: Poedit 1.8.10\n" 13 | "X-Poedit-KeywordsList: __;_e;__ngettext:1,2;_n:1,2;__ngettext_noop:1,2;" 14 | "_n_noop:1,2;_c,_nc:4c,1,2;_x:1,2c;_nx:4c,1,2;_nx_noop:4c,1,2;_ex:1,2c;" 15 | "esc_attr__;esc_attr_e;esc_attr_x:1,2c;esc_html__;esc_html_e;esc_html_x:1,2c\n" 16 | "X-Poedit-Basepath: .\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 18 | "X-Poedit-SearchPath-0: .\n" 19 | "X-Poedit-SearchPath-1: ..\n" 20 | 21 | #: ../class-gf-field-repeater-end.php:79 22 | msgid "" 23 | "The HTML to replace the default add button. This HTML will be placed inside " 24 | "of a span tag with the class 'gf_repeater_add'." 25 | msgstr "" 26 | "HTML:en som ska ersätta standard \"lägg till\"-knappen. Denna HTML kommer " 27 | "att placeras inuti en span-tagg med klassen 'gf_repeater_add'." 28 | 29 | #: ../class-gf-field-repeater-end.php:80 30 | msgid "" 31 | "The HTML to replace the default remove button. This HTML will be placed " 32 | "inside of a span tag with the class 'gf_repeater_remove'." 33 | msgstr "" 34 | "HTML:en som ska ersätta standard \"ta bort\"-knappen. Denna HTML kommer att " 35 | "placeras inuti en span-tagg med klassen 'gf_repeater_remove'." 36 | 37 | #: ../class-gf-field-repeater-end.php:81 38 | msgid "" 39 | "If enabled, no add and remove buttons will be displayed. This is useful if " 40 | "you plan on using custom javascript to trigger the repeater." 41 | msgstr "" 42 | "Om aktiverad så kommer inga \"lägg till\"- eller \"ta bort\"-knappar att " 43 | "visas. Detta är användbart om du planerar på att använda eget javascript för " 44 | "att trigga repeatern." 45 | 46 | #: ../class-gf-field-repeater.php:40 47 | msgid "This field is required." 48 | msgstr "Fältet är obligatoriskt." 49 | 50 | #: ../class-gf-field-repeater.php:160 51 | msgid "" 52 | "The number of times the repeater will be repeated when the form is rendered. " 53 | "Leaving this field blank or setting it to a number higher than the maximum " 54 | "number is the same as setting it to 1." 55 | msgstr "" 56 | "Antalet gånger denna repeater ska repeatas när formuläret renderas. Att " 57 | "lämna fältet tomt eller sätta ett nummer högre än det maximala numret är " 58 | "detsamma som att sätta det till 1." 59 | 60 | #: ../class-gf-field-repeater.php:161 61 | msgid "" 62 | "The minimum number of times the repeater is allowed to be repeated. Leaving " 63 | "this field blank or setting it to a number higher than the maximum field is " 64 | "the same as setting it to 1." 65 | msgstr "" 66 | "Minst antal gånger den här repeatern får repeatas. Att lämna fältet tomt " 67 | "eller sätta ett nummer högre än det maximala numret är detsamma som att " 68 | "sätta det till 1." 69 | 70 | #: ../class-gf-field-repeater.php:162 71 | msgid "" 72 | "The maximum number of times the repeater is allowed to be repeated. Leaving " 73 | "this field blank or setting it to a number lower than the minimum field is " 74 | "the same as setting it to unlimited." 75 | msgstr "" 76 | "Maximalt antal gånger den här repeatern får repeatas. Att lämna fältet tomt " 77 | "eller sätta ett nummer lägre än det minimala numret är detsamma som att " 78 | "sätta det till oändligt." 79 | 80 | #: ../class-gf-field-repeater.php:163 81 | msgid "" 82 | "A JavaScript object to be passed for animation settings. For advanced users " 83 | "only. Do not include initial brackets." 84 | msgstr "" 85 | "Ett JavaScript-object som ska skickas in för animationsinställningar. Endast " 86 | "för avancerade användare. Inkludera inte initiala brackets." 87 | 88 | #: ../class-gf-field-repeater.php:164 89 | msgid "" 90 | "If this is checked, the repeater label and description will not be shown to " 91 | "users on the form." 92 | msgstr "" 93 | "Om den här är i-kryssad kommer repeaterns label och beskriving inte visas " 94 | "för användare på formuläret." 95 | 96 | #: ../class-gf-field-repeater.php:186 97 | #, php-format 98 | msgid "A minimum number of %s is required." 99 | msgstr "En siffra som är minst %s krävs." 100 | 101 | #: ../class-gf-field-repeater.php:192 102 | #, php-format 103 | msgid "A maximum number of %s is allowed." 104 | msgstr "En siffra som är max %s krävs." 105 | 106 | #: ../class-gf-field-repeater.php:249 107 | msgid "A required field was left blank." 108 | msgstr "Ett obligatoriskt fält har lämnats tomt." 109 | -------------------------------------------------------------------------------- /class-gf-field-repeater-end.php: -------------------------------------------------------------------------------- 1 | 35 | 40 | 41 | "; 42 | 43 | echo "
  • 44 | 49 | 50 |
  • "; 51 | } 52 | } 53 | 54 | public static function gform_appearance_settings($position, $form_id) { 55 | if ($position == 400) { 56 | echo "
  • 57 | 58 | 63 |
  • "; 64 | } 65 | } 66 | 67 | public static function gform_editor() { 68 | echo ""; 76 | } 77 | 78 | public static function gform_tooltips($tooltips) { 79 | $tooltips['form_field_repeater_end_add'] = __( "The HTML to replace the default add button. This HTML will be placed inside of a span tag with the class 'gf_repeater_add'.", 'gravityformsrepeater' ); 80 | $tooltips['form_field_repeater_end_remove'] = __( "The HTML to replace the default remove button. This HTML will be placed inside of a span tag with the class 'gf_repeater_remove'.", 'gravityformsrepeater' ); 81 | $tooltips['form_field_repeater_end_hideButtons'] = __( "If enabled, no add and remove buttons will be displayed. This is useful if you plan on using custom javascript to trigger the repeater.", 'gravityformsrepeater' ); 82 | return $tooltips; 83 | } 84 | 85 | public function get_field_content($value, $force_frontend_label, $form) { 86 | if (is_admin()) { 87 | $admin_buttons = $this->get_admin_buttons(); 88 | 89 | $field_content = "{$admin_buttons} 90 |
    91 |
    end repeater
    92 |
    REPEATER
    93 |
    end of repeater
    94 |
    "; 95 | } else { 96 | $add_html = $this->add; 97 | $remove_html = $this->remove; 98 | $hideButtons = $this->hideButtons; 99 | $tabindex = GFCommon::get_tabindex(); 100 | 101 | if (empty($add_html)) { $add_html = "\"+\""; } 102 | if (empty($remove_html)) { $remove_html = "\"-\""; } 103 | 104 | $field_content = "
    \n"; 105 | 106 | if (!$hideButtons) { 107 | $field_content .= "{$remove_html}"; 108 | $field_content .= "{$add_html}"; 109 | } 110 | 111 | $field_content .= "
    "; 112 | } 113 | return $field_content; 114 | } 115 | } 116 | GF_Fields::register(new GF_Field_Repeater_End()); 117 | -------------------------------------------------------------------------------- /js/gf-repeater-admin.min.js: -------------------------------------------------------------------------------- 1 | function gfRepeater_editforms_getRepeaters(){var e,t,r=0,i=0,d=[],a=[];return jQuery(".gfield").each(function(){if(0==r)jQuery(this).has(".gf-repeater-start").length&&(i+=1,gfRepeater_debug&&console.log("Repeater #"+i+" - Start: "+jQuery(this).attr("id")),e=this.id,r=1);else{if(jQuery(this).has(".gf-repeater-start").length)return!1;jQuery(this).has(".gf-repeater-end").length?(gfRepeater_debug&&console.log("Repeater #"+i+" - End: "+jQuery(this).attr("id")),t=this.id,gfRepeater_repeaters[i]={startId:e,endId:t,childrenIds:a,children:d},d=[],a=[],r=0,e=null,t=null):(d.push(this),a.push(this.id),gfRepeater_debug&&console.log("Repeater #"+i+" - Child #"+d.length+" - Found: "+this.id))}}),0!==r?!1:!0}function gfRepeater_getId(e){return e.substr(6)}function gfRepeater_getField(e){var t=gfRepeater_getId(e);return GetFieldById(t)}function gfRepeater_getIndex(e){var t=!1,r=-1;return jQuery(".gfield").each(function(){return r+=1,this.id==e?(t=!0,!1):void 0}),t?r:!1}function gfRepeater_getRepeaterEndId(e){var t,r=!1;return jQuery(".gfield").each(function(){var i=gfRepeater_getField(this.id),d=gfRepeater_getId(this.id);r?"repeater-end"==i.type&&(t=this.id,r=!1):"repeater"==i.type&&d==e&&(r=!0)}),t?t:!1}function gfRepeater_isIndexInsideRepeater(e){var t=!1,r=!1,i=-1;return jQuery(".gfield").each(function(){i+=1;var d=gfRepeater_getField(this.id);t?("repeater-end"==d.type?(gfRepeater_debug&&console.log("Repeater End Index: "+i),t=!1):gfRepeater_debug&&console.log("Repeater Child Index: "+i),i==e&&(r=!0)):"repeater"==d.type&&(gfRepeater_debug&&console.log("Repeater Start Index: "+i),t=!0)}),t&&(r=!1),gfRepeater_debug&&console.log("Is the index "+e+" inside of a repeater? "+r),r}function gfRepeater_brokenRepeater(){var e=!1,t=!1,r=-1;return jQuery(".gfield").each(function(){r+=1;var i=gfRepeater_getField(this.id);if(e){if("repeater"==i.type)return alert("Nesting Repeaters is not supported!"),t=!0,!1;if("repeater-end"==i.type)e=!1;else{var d=gfRepeater_editforms_fieldCheck(i.type,r);if(!d)return t=!0,!1}}else if("repeater"==i.type)e=!0;else if("repeater-end"==i.type)return alert("One of your repeaters are missing their repeater start field!"),t=!0,!1}),!t&&e&&(alert("One of your repeaters are missing their repeater end field!"),t=!0),t}function gfRepeater_editforms_update(e){gfRepeater_editforms_getRepeaters()&&(e&&UpdateFormObject(),jQuery.each(gfRepeater_repeaters,function(t,r){var i=gfRepeater_getField(this.startId),d=[],a=[];jQuery.each(this.children,function(t,r){var i=gfRepeater_editforms_updateRequired(r,e),n=gfRepeater_getId(r.id);i&&a.push(n),d.push(n)}),i.repeaterRequiredChildren=a,i.repeaterChildren=d}))}function gfRepeater_editforms_updateRequired(e,t){var r=(gfRepeater_getId(e.id),gfRepeater_getField(e.id)),i=r.isRequired,d=r.repeaterField_isRequired,a=!1;return gfRepeater_debug&&console.log(e.id+" - Before Filter - isRequired:"+r.isRequired+" - repeaterField_isRequired:"+r.repeaterField_isRequired),i&&t&&(r.isRequired=!1,r.repeaterField_isRequired=!0,gfRepeater_debug&&console.log(e.id+" - Used Filter 1"),a=!0),!i&&t&&(r.repeaterField_isRequired=!1,gfRepeater_debug&&console.log(e.id+" - Used Filter 2")),d&&!t&&(jQuery(e).find(".gfield_required").html("*"),r.isRequired=!0,r.repeaterField_isRequired=!1,gfRepeater_debug&&console.log(e.id+" - Used Filter 3")),d||t||(jQuery(e).find(".gfield_required").html(""),r.isRequired=!1,gfRepeater_debug&&console.log(e.id+" - Used Filter 4")),gfRepeater_debug&&console.log(e.id+" - After Filter - isRequired:"+r.isRequired+" - repeaterField_isRequired:"+r.repeaterField_isRequired),a}function gfRepeater_editforms_fieldCheck(e,t){var r=["text","textarea","select","multiselect","number","checkbox","radio","hidden","html","section","name","date","time","phone","address","website","email","product","quanity","option","shipping","total"];if(gfRepeater_isIndexInsideRepeater(t)){if("repeater"==e)return alert("Nesting repeaters is not supported!"),!1;if("repeater-end"!==e&&-1==jQuery.inArray(e,r))return alert("Sorry, the "+e+" field is not supported by the Gravity Forms Repeater Add-On at this time. :("),!1}return!0}function gfRepeater_editforms_patch_SaveForm(){var e=SaveForm;SaveForm=function(t){return gfRepeater_brokenRepeater()?!1:(gfRepeater_editforms_update(!0),e.call(this,t))}}function gfRepeater_editforms_patch_StartAddField(){var e=StartAddField;StartAddField=function(t,r){var i=gfRepeater_editforms_fieldCheck(t,r);return i?e.call(this,t,r):void jQuery("#gform_adding_field_spinner").remove()}}function gfRepeater_editforms_patch_EndAddField(){var e=EndAddField;EndAddField=function(t,r,i){e.call(this,t,r,i),"repeater"==t.type&&StartAddField("repeater-end",i+1)}}function gfRepeater_editforms_patch_StartDeleteField(){var e=StartDeleteField;StartDeleteField=function(t){var r=jQuery(t)[0].id.split("_")[2],i=GetFieldById(r);return"repeater-end"==i.type?void alert("This field cannot be deleted directly. Please delete the repeater start field first."):void e.call(this,t)}}function gfRepeater_editforms_patch_EndDeleteField(){var e=EndDeleteField;EndDeleteField=function(t){var r=GetFieldById(t);if("repeater"==r.type)var i=gfRepeater_getRepeaterEndId(t);e.call(this,t),"repeater"==r.type&&i&&(i=gfRepeater_getId(i),DeleteField(i))}}function gfRepeater_editforms_patch_SortUpdate(){jQuery("#gform_fields").sortable({update:function(e,t){gfRepeater_brokenRepeater()&&jQuery("#gform_fields").sortable("cancel")}})}var gfRepeater_debug=!1,gfRepeater_repeaters={},gfRepeater_page=gf_repeater_js_admin_strings.page;jQuery(document).ready(function(e){"gf_edit_forms"==gfRepeater_page&&gfRepeater_editforms_update(!1)}),jQuery(window).load(function(){"gf_edit_forms"==gfRepeater_page&&(gfRepeater_editforms_patch_SaveForm(),gfRepeater_editforms_patch_StartAddField(),gfRepeater_editforms_patch_EndAddField(),gfRepeater_editforms_patch_StartDeleteField(),gfRepeater_editforms_patch_EndDeleteField(),gfRepeater_editforms_patch_SortUpdate())}),gfRepeater_debug&&jQuery(window).keydown(function(e){38==e.which&&console.log(gfRepeater_repeaters),40==e.which&&jQuery(".gfield").each(function(){console.log(gfRepeater_getField(this.id))})}); -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | v1.1.0 (TBA) 2 | - Fixed bug where sometimes a javascript error would occur in the form editor when trying to save a form after deleting a repeater. 3 | - Fixed bug where sometimes a 'Undefined Index "conditionalLogic"' error is shown when submitting a form. 4 | - Fixed bug where repeated field values would be cleared if on a multipage form. (Issue #21) 5 | - Fixed bug where a PHP error is shown when viewing an entry's details if no repeater child fields have been filled out. (Issue #24) (Thanks @WebImage!) 6 | - Fixed bug where repeated field tab indexes would be set to "NaN" if tab indexes are disabled. (Issue #28) 7 | - Fixed bug where javascript erros would occur if there was more than one form on the same page. 8 | - Fixed bug where javascript functions would try to do things on other forms on the same page even if they didn't have a repeater field. 9 | - Fixed repeater start pre-populate setting. 10 | - gfRepeater_getChoiceValue now gets the input values instead of labels. (Issue #22) 11 | - Form editor will no longer let you screw up repeater fields and will notify you if you are trying to do something you shouldn't. (ie. Nesting repeaters, adding unsupported fields, etc.) 12 | - Repeater button in the form editor has been moved to the "Advanced Fields" section. 13 | - Repeater End button has been removed. Repeater end fields are now automatically added and deleted with the repeater start fields. 14 | - Repeated fields with multiple inputs are now stored with their "subId" as key instead of just an increasing numeric key. 15 | - Added support for conditional logic on the repeater start field. Repeater children and repeater end fields are also effected by their repeater start field. 16 | - Added support for fields with "max length" set. 17 | - Added support for fields with "enhanced user interface" enabled. 18 | - Added gform_editor_repeater_field_settings and gform_editor_repeater_end_field_settings filters. 19 | - Added support for animations! (Docs coming soon!) 20 | 21 | v1.0.9 (1/25/2016) 22 | - Fixed bug where in some cases a PHP error would occur when attempting to automatically disable ajax on a form. (Issue #18) 23 | - Fixed bug where sometimes a javascript error would occur on date fields with a calendar icon. (Issue #15) 24 | - Fixed bug where removing a specific repeatId would cause some weird things to happen. 25 | - Fixed bug where repeaters in a multipage form that are not on the first page would appear to have failed validation even if the form hasn't been submitted. 26 | - Fixed bug where repeaters in a muttipage form that are not on the first page would forget their saved data and ignore their start setting. 27 | - Fixed hideButtons setting. 28 | - Added [gfRepeater-buttons], [gfRepeater-add], and [gfRepeater-remove] shortcodes. 29 | - Added custom CSS option to repeater start and end fields. 30 | - Added gf_repeater_hide_add and gf_repeater_hide_remove CSS classes. 31 | - Added gfRepeater_select javascript function. 32 | - Added 'gform_repeater_init_done' trigger to window and individual forms. 33 | - Changed 'beforeRepeat', 'afterRepeat', 'beforeUnRepeat', and 'afterUnRepeat' triggers to 'gform_repeater_before_repeat', 'gform_repeater_after_repeat', 'gform_repeater_before_unrepeat', and 'gform_repeater_after_unrepeat'. 34 | - Repeated fields now respect conditional logic from outside the repeater. 35 | - Applying conditional logic on a repeated section field now also applys that logic on repeated fields under that section. 36 | - When exporting entries, repeater child data is now exported in a comma-separated list. 37 | 38 | v1.0.8 (12/30/2015) 39 | - Fixed bug where sometimes for some reason an input name would be sent as an array and cause a PHP error to display. (Issue #6) 40 | - Fixed bug where sometimes validation message would be displayed multiple times for same field. 41 | - Fixed bug where filters would not be loaded if Repeater was the first field on the form. 42 | - Fixed bug where sometimes captured repeater input data would be applied to other forms in cases where there are multiple forms on one page. 43 | - Fixed prepopulation for checkbox and radio fields. 44 | - Changed the way settings and field data is sent to Javascript so now monkey patching the input mask script is no longer necessary. 45 | - Field input masks are now stored in gfRepeaters data. (gfRepeaters[repeaterId]['children'][childId]['inputMask']) 46 | - Added support for conditional logic enabled fields! 47 | - Added support for Repeater merge tags. 48 | - Cleaned up formatting for text-only email notifications. 49 | - Changed Repeater End Setting from 'doNotUse' to 'hideButtons'. 50 | - Added Repeater Start Setting 'Hide Label & Description' to appearance tab. 51 | - Repeater label now defaults to 'Repeater' instead of blank and adminLabel now defaults to blank instead of 'Repeater'. 52 | - Repeater Javascript now initiates after 'gform_post_render' has been triggered instead of on window load. 53 | 54 | v1.0.7.2 (12/18/2015) 55 | - Fixed saved repeater field data during validation. (Accidently broke it in the 1.0.7.1 patch) 56 | 57 | v1.0.7.1 (12/18/2015) 58 | - Fixed a bug where repeater would break multipage forms. (Sorry about that!) 59 | 60 | v1.0.7 (12/17/2015) 61 | - Fixed bug where field order was not saved when a form was submitted. 62 | - Fixed syntax error for older versions of PHP. 63 | - Repeated fields now have their tabindex set to the one that was assigned to the repeater start field. 64 | - Repeater start field tabindex now stored in gfRepeaters data. (gfRepeaters[repeaterId]['data']['tabIndex']) 65 | - Repeater is now repeated if a paremeter is set for a specific field that is higher than the start repeat count. 66 | - Highest set paremeter number is now stored in gfRepeaters data. (gfRepeaters[repeaterId]['data']['paremCount']) 67 | - Repeater dependencies, actions, and filters are now only loaded on forms that have a repeater field. 68 | - Field type is now set for hidden and section fields in gfRepeaters data. 69 | - Ajax is now automatically disabled on forms that have a repeater field. (Until I come up with a fix for ajax enabled forms) 70 | - Added support for Andy Fragen's GitHub Updater. (See readme for more info) 71 | - CSS is now minified. 72 | - gfRepeaters data now contains the form ID number as a root and the repeater Id as a subroot. (See readme for more info) 73 | 74 | Release Notes: 75 | - Javascript functions now require the form ID to be specified. (See readme for more info) 76 | 77 | v1.0.6 (12/7/2015) 78 | - Fixed repeater 'start' setting. (Apparently it's been broken for awhile?) 79 | - Fixed datepicker ui for repeated date fields. 80 | - Fixed bug where repeater plugin wouldn't allow page settings to display in the form editor. 81 | - Fixed entry detail formatting for emails and non-html views. 82 | - Repeated field labels are no longer saved in the database with the entries and are now looked up by field ID. 83 | - Added support for Hidden field type. 84 | - Added the ability to change the repeater 'start' setting by using the built in Gravity Froms pre-populate filters and hooks. 85 | - Added the ability to pre-populate repeated fields using the built in Gravity Forms filters and hooks. (See readme for more info) 86 | - Field ID number is now stored in gfRepeaters data. (gfRepeaters[repeaterId]['children'][childId]['idNum']) 87 | - Field input pre-populate value is now stored in gfRepeaters data. (gfRepeaters[repeaterId]['children'][childId]['inputs'][inputId]['prePopulate']) (See readme for more info) 88 | - Plugin now requires Gravity Forms 1.9 or later. 89 | 90 | v1.0.5 (11/19/2015) 91 | - Fixed bug where sometimes not all repeated, required fields would be validated. 92 | - Fixed bug where repeated fields that are set to 'adminOnly' would have that setting switched off upon form validation. 93 | - Added support for MultiSelect, Radio, and Time fields. 94 | - Added upgrade function to clean up entries from older versions. 95 | - Repeater field admin label now defaults to 'Repeater' instead of blank. 96 | - All repeated values are now stored as arrays besides special cases such as Section fields. 97 | - Field types are now stored in gfRepeaters data. (gfRepeaters[repeaterId]['children'][childId]['type']) 98 | - Name and Address fields now only require the normally required fields. 99 | - Added plugin page link to row meta. 100 | - A few misc things and code clean up. 101 | 102 | v1.0.4 (11/6/2015) 103 | - Fixed bug with number field where a PHP error would show up if a form failed validation. 104 | - Fixed bug where a javascript error would occur if no repeater children were marked as required. 105 | - Fixed bug where a PHP error would sometimes occur on the entries page in the admin panel if a repeated field was submitted blank. 106 | - Added support for Section field type. 107 | - Default validation functions are now bypassed for repeater children. 108 | - gfRepeater-count shortcode now shows up for field titles on the entires page in the admin panel. 109 | - Blank repeated fields now show up if "Show Empty Fields" is enabled. 110 | - Repeater children and repeater end fields no longer display duplicated results on the entries page in the admin panel. 111 | - Added a little bit of backwards compatibility for fetching saved repeater child entry data. 112 | 113 | Release Notes: 114 | - The entries page in the admin panel may still display duplicated fields for repeater children and repeater end fields until the form has been updated again. Just go to the edit form page and click "Update Form" to fix this. 115 | 116 | v1.0.3 (10/30/2015) 117 | - Fixed bug where repeated fields would look like they failed validation if any repeated fields before them failed. 118 | - Fixed bug with validation on fields with multiple inputs. 119 | - Fixed bugs with form editor and setting repeater children required option. 120 | - Added beforeRepeat, afterRepeat, beforeUnRepeat, and AfterUnRepeat form triggers. 121 | - Added a stylesheet with some default styles for repeater releated stuff. 122 | - Added keypress event and tab index to add and remove buttons. 123 | - Added a little bit of error handing to the javascript just in case the admin forgets to add a repeater-end or trys to nest repeaters. 124 | - Repeater start field label now defaults to blank instead of "Untitled". 125 | 126 | v1.0.2 (10/27/2015) 127 | - Fixed bug where if any fields in a form with a repeater in it would be unsubmittable. 128 | - Fixed repeater field validation. 129 | - Added custom validation message feature. 130 | - Added repeater "start" number feature. 131 | - Added repeater label and description options. 132 | - Added custom Add and Remove button HTML feature. 133 | - Added "Do not use add and remove buttons" feature. 134 | - Added new "[gfRepeater-count]" shortcode. 135 | 136 | v1.0.1 (10/20/2015) 137 | - Added support for Address, Checkboxes, Name, and Number fields. 138 | - Added View details and GitHub links to row meta. 139 | - Fixed input masks! 140 | 141 | v1.0.0 (10/11/2015) 142 | - Initial Release -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Gravity Forms Repeater Add-On === 2 | Contributors: KodieGrantham 3 | Tags: gravityforms, gravity, forms, form, gravityforms, repeater, repeat, duplicate, duplication, field, fields 4 | Requires at least: 3.9 5 | Tested up to: 4.4 6 | Stable tag: 1.0.9 7 | 8 | A Gravity Forms add-on that allows specified groups of fields to be repeated by the user. 9 | 10 | == WAIT! == 11 | **This plugin is no longer maintained and is probably broken, go use [Gravity Forms Nested Forms by Gravity Wiz](https://gravitywiz.com/documentation/gravity-forms-nested-forms/?ref=145) instead!** 12 | 13 | == Description == 14 | A Gravity Forms add-on that allows specified groups of fields to be repeated by the user. 15 | 16 | = Supported Fields = 17 | * Address 18 | * Checkboxes 19 | * Date 20 | * Drop Down 21 | * Email 22 | * Hidden 23 | * HTML 24 | * MultiSelect 25 | * Name 26 | * Number 27 | * Paragraph Text 28 | * Phone 29 | * Radio 30 | * Section 31 | * Single Line Text 32 | * Time 33 | * Website 34 | 35 | = Features = 36 | * Repeat groups of fields multiple times 37 | * Use multiple repeaters on the same form 38 | * Use shortcodes to display data to the user 39 | * Use Javascript to manipulate the repeater (See GitHub Page) 40 | * Customize the add and remove button's HTML 41 | * Use Gravity Forms pre-populate hooks and filters like normal 42 | * Supports Conditional Logic! 43 | 44 | = Issues = 45 | * Not all fields are currently supported. 46 | * Ajax enabled forms are not yet supported. (Ajax will be automatically disabled on forms with repeaters) 47 | 48 | = Shortcodes = 49 | You can place shortcodes inside of input labels, input descriptions, and HTML blocks! 50 | 51 | * [gfRepeater-count] - Will output the current repeat number. 52 | * [gfRepeater-buttons] - Will output both the '+' and '-' buttons. 53 | * [gfRepeater-add] - Will output the '+' button. 54 | * [gfRepeater-remove] - Will output the '-' button. 55 | 56 | = CSS Classes = 57 | You can use these CSS classes in the in the "Custom CSS Class" setting to do different things. 58 | 59 | * gf_repeater_hide_add - Will hide the '+' button if placed in the repeater end css setting. 60 | * gf_repeater_hide_remove - Will hide the '-' button if placed in the repeater end css setting. 61 | 62 | = Check out the source on GitHub! = 63 | 64 | 65 | == Installation == 66 | 1. Upload the `repeater-add-on-for-gravity-forms` folder to the `/wp-content/plugins/` directory. 67 | 1. Activate the plugin through the 'Plugins' menu in WordPress. 68 | 69 | == Frequently Asked Questions == 70 | = Can I use multiple repeaters in one form? = 71 | Yes! 72 | 73 | = Can I nest repeaters? = 74 | Unfortunately nesting repeaters is not supported at this time. 75 | 76 | = Can I change the `+` and `-` buttons to text links? = 77 | Yes! Just go to the form editor and change the `Add HTML` and `Remove HTML` settings to `Your Link Text` and they should appear as regular links on your form! 78 | 79 | == Screenshots == 80 | 1. Any supported fields placed between the `Repeater` and `Repeater End` will be repeated. 81 | 2. A minimum and maximum number may be set to limit how many times a Repeater can be repeated. 82 | 3. When activated, 2 new field type buttons will be added to Gravity Forms, `Repeater` and `Repeater End`. 83 | 4. A view of the front-end. The only visible difference will be the added `+` and `-` buttons. 84 | 85 | == Changelog == 86 | = 1.0.9 = 87 | * Fixed bug where in some cases a PHP error would occur when attempting to automatically disable ajax on a form. 88 | * Fixed bug where sometimes a javascript error would occur on date fields with a calendar icon. 89 | * Fixed bug where removing a specific repeatId would cause some weird things to happen. 90 | * Fixed bug where repeaters in a multipage form that are not on the first page would appear to have failed validation even if the form hasn't been submitted. 91 | * Fixed bug where repeaters in a muttipage form that are not on the first page would forget their saved data and ignore their start setting. 92 | * Fixed hideButtons setting. 93 | * Added [gfRepeater-buttons], [gfRepeater-add], and [gfRepeater-remove] shortcodes. 94 | * Added custom CSS option to repeater start and end fields. 95 | * Added gf_repeater_hide_add and gf_repeater_hide_remove CSS classes. 96 | * Added gfRepeater_select javascript function. 97 | * Added 'gform_repeater_init_done' trigger to window and individual forms. 98 | * Changed 'beforeRepeat', 'afterRepeat', 'beforeUnRepeat', and 'afterUnRepeat' triggers to 'gform_repeater_before_repeat', 'gform_repeater_after_repeat', 'gform_repeater_before_unrepeat', and 'gform_repeater_after_unrepeat'. 99 | * Repeated fields now respect conditional logic from outside the repeater. 100 | * Applying conditional logic on a repeated section field now also applys that logic on repeated fields under that section. 101 | * When exporting entries, repeater child data is now exported in a comma-separated list. 102 | 103 | = 1.0.8 = 104 | * Fixed bug where sometimes for some reason an input name would be sent as an array and cause a PHP error to display. 105 | * Fixed bug where sometimes validation message would be displayed multiple times for same field. 106 | * Fixed bug where filters would not be loaded if Repeater was the first field on the form. 107 | * Fixed bug where sometimes captured repeater input data would be applied to other forms in cases where there are multiple forms on one page. 108 | * Fixed prepopulation for checkbox and radio fields. 109 | * Changed the way settings and field data is sent to Javascript so now monkey patching the input mask script is no longer necessary. 110 | * Field input masks are now stored in gfRepeaters data. (gfRepeaters[repeaterId]['children'][childId]['inputMask']) 111 | * Added support for conditional logic enabled fields! 112 | * Added support for Repeater merge tags. 113 | * Cleaned up formatting for text-only email notifications. 114 | * Changed Repeater End Setting from 'doNotUse' to 'hideButtons'. 115 | * Added Repeater Start Setting 'Hide Label & Description' to appearance tab. 116 | * Repeater label now defaults to 'Repeater' instead of blank and adminLabel now defaults to blank instead of 'Repeater'. 117 | * Repeater Javascript now initiates after 'gform_post_render' has been triggered instead of on window load. 118 | 119 | = 1.0.7.2 = 120 | * Fixed saved repeater field data during validation. (Accidently broke it in the 1.0.7.1 patch) 121 | 122 | = 1.0.7.1 = 123 | * Fixed a bug where repeater would break multipage forms. (Sorry about that!) 124 | 125 | = 1.0.7 = 126 | * Fixed bug where field order was not saved when a form was submitted. 127 | * Fixed syntax error for older versions of PHP. 128 | * Repeated fields now have their tabindex set to the one that was assigned to the repeater start field. 129 | * Repeater start field tabindex now stored in gfRepeaters data. (gfRepeaters[repeaterId]['data']['tabIndex']) 130 | * Repeater is now repeated if a paremeter is set for a specific field that is higher than the start repeat count. 131 | * Highest set paremeter number is now stored in gfRepeaters data. (gfRepeaters[repeaterId]['data']['paremCount']) 132 | * Repeater dependencies, actions, and filters are now only loaded on forms that have a repeater field. 133 | * Field type is now set for hidden and section fields in gfRepeaters data. 134 | * Ajax is now automatically disabled on forms that have a repeater field. (Until I come up with a fix for ajax enabled forms) 135 | * Added support for Andy Fragen's GitHub Updater. (See readme for more info) 136 | * CSS is now minified. 137 | * gfRepeaters data now contains the form ID number as a root and the repeater Id as a subroot. (See readme for more info) 138 | 139 | = 1.0.6 = 140 | * Fixed repeater 'start' setting. (Apparently it's been broken for awhile?) 141 | * Fixed datepicker ui for repeated date fields. 142 | * Fixed bug where repeater plugin wouldn't allow page settings to display in the form editor. 143 | * Fixed entry detail formatting for emails and non-html views. 144 | * Repeated field labels are no longer saved in the database with the entries and are now looked up by field ID. 145 | * Added support for Hidden field type. 146 | * Added the ability to change the repeater 'start' setting by using the built in Gravity Froms pre-populate filters and hooks. 147 | * Added the ability to pre-populate repeated fields using the built in Gravity Forms filters and hooks. (See readme for more info) 148 | * Field ID number is now stored in gfRepeaters data. (gfRepeaters[repeaterId]['children'][childId]['idNum']) 149 | * Field input pre-populate value is now stored in GfRepeaters data. (gfRepeaters[repeaterId]['children'][childId]['inputs'][inputId]['prePopulate']) (See readme for more info) 150 | * Plugin now requires Gravity Forms 1.9 or later. 151 | 152 | = 1.0.5 = 153 | * Fixed bug where sometimes not all repeated, required fields would be validated. 154 | * Fixed bug where repeated fields that are set to 'adminOnly' would have that setting switched off upon form validation. 155 | * Added support for MultiSelect, Radio, and Time fields. 156 | * Added upgrade function to clean up entries from older versions. 157 | * Repeater field admin label now defaults to 'Repeater' instead of blank. 158 | * All repeated values are now stored as arrays besides special cases such as Section fields. 159 | * Field types are now stored in gfRepeaters data. (gfRepeaters[repeaterId]['children'][childId]['type']) 160 | * Name and Address fields now only require the normally required fields. 161 | * Added plugin page link to row meta. 162 | * A few misc things and code clean up. 163 | 164 | = 1.0.4 = 165 | * Fixed bug with number field where a PHP error would show up if a form failed validation. 166 | * Fixed bug where a javascript error would occur if no repeater children were marked as required. 167 | * Fixed bug where a PHP error would sometimes occur on the entries page in the admin panel if a repeated field was submitted blank. 168 | * Added support for Section field type. 169 | * Default validation functions are now bypassed for repeater children. 170 | * gfRepeater-count shortcode now shows up for field titles on the entires page in the admin panel. 171 | * Blank repeated fields now show up if "Show Empty Fields" is enabled. 172 | * Repeater children and repeater end fields no longer display duplicated results on the entries page in the admin panel. 173 | * Added a little bit of backwards compatibility for fetching saved repeater child entry data. 174 | 175 | = 1.0.3 = 176 | * Fixed bug where repeated fields would look like they failed validation if any repeated fields before them failed. 177 | * Fixed bug with validation on fields with multiple inputs. 178 | * Fixed bugs with form editor and setting repeater children required option. 179 | * Added beforeRepeat, afterRepeat, beforeUnRepeat, and AfterUnRepeat form triggers. 180 | * Added a stylesheet with some default styles for repeater releated stuff. 181 | * Added keypress event and tab index to add and remove buttons. 182 | * Added a little bit of error handing to the javascript just in case the admin forgets to add a repeater-end or trys to nest repeaters. 183 | * Repeater start field label now defaults to blank instead of "Untitled". 184 | 185 | = 1.0.2 = 186 | * Fixed bug where if any fields in a form with a repeater in it would be unsubmittable. 187 | * Fixed repeater field validation. 188 | * Added custom validation message feature. 189 | * Added repeater "start" number feature. 190 | * Added repeater label and description options. 191 | * Added custom Add and Remove button HTML feature. 192 | * Added "Do not use add and remove buttons" feature. 193 | * Added new "[gfRepeater-count]" shortcode. 194 | 195 | = 1.0.1 = 196 | * Added support for Address, Checkboxes, Name, and Number fields. 197 | * Added View details and GitHub links to row meta. 198 | * Fixed input masks! 199 | 200 | = 1.0.0 = 201 | * Initial Release 202 | -------------------------------------------------------------------------------- /js/gf-repeater-admin.js: -------------------------------------------------------------------------------- 1 | var gfRepeater_debug = gf_repeater_js_admin_strings.debug; 2 | var gfRepeater_repeaters = {}; 3 | var gfRepeater_page = gf_repeater_js_admin_strings.page; 4 | 5 | function gfRepeater_editforms_getRepeaters() { 6 | var repeaterFound = 0; 7 | var repeaterId = 0; 8 | var repeaterChildren = []; 9 | var repeaterChildrenIds = []; 10 | var repeaterStartId; 11 | var repeaterEndId; 12 | 13 | jQuery('.gfield').each(function(){ 14 | if (repeaterFound == 0) { 15 | if (jQuery(this).has('.gf-repeater-start').length) { 16 | repeaterId += 1; 17 | if (gfRepeater_debug) { console.log('Repeater #'+repeaterId+' - Start: '+jQuery(this).attr('id')); } 18 | repeaterStartId = this.id; 19 | repeaterFound = 1; 20 | } 21 | } else { 22 | if (jQuery(this).has('.gf-repeater-start').length) { return false; } 23 | if (jQuery(this).has('.gf-repeater-end').length) { 24 | if (gfRepeater_debug) { console.log('Repeater #'+repeaterId+' - End: '+jQuery(this).attr('id')); } 25 | repeaterEndId = this.id; 26 | gfRepeater_repeaters[repeaterId] = {startId:repeaterStartId,endId:repeaterEndId,childrenIds:repeaterChildrenIds,children:repeaterChildren}; 27 | 28 | repeaterChildren = []; 29 | repeaterChildrenIds = []; 30 | repeaterFound = 0; 31 | repeaterStartId = null; 32 | repeaterEndId = null; 33 | } else { 34 | repeaterChildren.push(this); 35 | repeaterChildrenIds.push(this.id); 36 | if (gfRepeater_debug) { console.log('Repeater #'+repeaterId+' - Child #'+repeaterChildren.length+' - Found: '+this.id); } 37 | } 38 | } 39 | }); 40 | 41 | if (repeaterFound !== 0) { return false; } 42 | return true; 43 | } 44 | 45 | function gfRepeater_getId(id) { 46 | return id.substr(6); 47 | } 48 | 49 | function gfRepeater_getField(id) { 50 | var idNum = gfRepeater_getId(id); 51 | return GetFieldById(idNum); 52 | } 53 | 54 | function gfRepeater_getIndex(id) { 55 | var idFound = false; 56 | var x = -1; 57 | 58 | jQuery('.gfield').each(function(){ 59 | x += 1; 60 | 61 | if (this.id == id) { 62 | idFound = true; 63 | return false; 64 | } 65 | }); 66 | 67 | if (idFound) { 68 | return x; 69 | } else { 70 | return false; 71 | } 72 | } 73 | 74 | function gfRepeater_getRepeaterEndId(id) { 75 | var pointerInRepeater = false; 76 | var repeaterEndId; 77 | 78 | jQuery('.gfield').each(function(){ 79 | var field = gfRepeater_getField(this.id); 80 | var fieldId = gfRepeater_getId(this.id); 81 | 82 | if (!pointerInRepeater) { 83 | if (field.type == 'repeater' && fieldId == id) { 84 | pointerInRepeater = true; 85 | } 86 | } else { 87 | if (field.type == 'repeater-end') { 88 | repeaterEndId = this.id; 89 | pointerInRepeater = false; 90 | } 91 | } 92 | }); 93 | 94 | if (repeaterEndId) { 95 | return repeaterEndId; 96 | } else { 97 | return false; 98 | } 99 | } 100 | 101 | function gfRepeater_isIndexInsideRepeater(index) { 102 | var pointerInRepeater = false; 103 | var indexInRepeater = false; 104 | var x = -1; 105 | 106 | jQuery('.gfield').each(function(){ 107 | x += 1; 108 | var field = gfRepeater_getField(this.id); 109 | 110 | if (!pointerInRepeater) { 111 | if (field.type == 'repeater') { 112 | if (gfRepeater_debug) { console.log('Repeater Start Index: ' + x); } 113 | pointerInRepeater = true; 114 | } 115 | } else { 116 | if (field.type == 'repeater-end') { 117 | if (gfRepeater_debug) { console.log('Repeater End Index: ' + x); } 118 | pointerInRepeater = false; 119 | } else { 120 | if (gfRepeater_debug) { console.log('Repeater Child Index: ' + x); } 121 | } 122 | 123 | if (x == index) { 124 | indexInRepeater = true; 125 | } 126 | } 127 | }); 128 | 129 | if (pointerInRepeater) { 130 | indexInRepeater = false; 131 | } 132 | 133 | if (gfRepeater_debug) { console.log('Is the index ' + index + ' inside of a repeater? ' + indexInRepeater); } 134 | 135 | return indexInRepeater; 136 | } 137 | 138 | function gfRepeater_brokenRepeater() { 139 | var pointerInRepeater = false; 140 | var brokenRepeater = false; 141 | var x = -1; 142 | 143 | jQuery('.gfield').each(function(){ 144 | x += 1; 145 | var field = gfRepeater_getField(this.id); 146 | 147 | if (!pointerInRepeater) { 148 | if (field.type == 'repeater') { 149 | pointerInRepeater = true; 150 | } else if (field.type == 'repeater-end') { 151 | alert('One of your repeaters are missing their repeater start field!'); 152 | brokenRepeater = true; 153 | return false; 154 | } 155 | } else { 156 | if (field.type == 'repeater') { 157 | alert('Nesting Repeaters is not supported!'); 158 | brokenRepeater = true; 159 | return false; 160 | } else if (field.type == 'repeater-end') { 161 | pointerInRepeater = false; 162 | } else { 163 | var fieldCheck = gfRepeater_editforms_fieldCheck(field.type, x); 164 | if (!fieldCheck) { 165 | brokenRepeater = true; 166 | return false; 167 | } 168 | } 169 | } 170 | }); 171 | 172 | if (!brokenRepeater && pointerInRepeater) { 173 | alert('One of your repeaters are missing their repeater end field!'); 174 | brokenRepeater = true; 175 | } 176 | 177 | return brokenRepeater; 178 | } 179 | 180 | function gfRepeater_editforms_update(leaving) { 181 | if (!gfRepeater_editforms_getRepeaters()) { return; } 182 | if (leaving) { UpdateFormObject(); } 183 | 184 | jQuery.each(gfRepeater_repeaters, function(key, value){ 185 | var repeaterId = key; 186 | var repeaterField = gfRepeater_getField(this['startId']); 187 | var repeaterChildren = []; 188 | var requiredChildren = []; 189 | 190 | jQuery.each(this['children'], function(key, value){ 191 | var fieldRequired = gfRepeater_editforms_updateRequired(value, leaving); 192 | var fieldId = gfRepeater_getId(value.id); 193 | if (fieldRequired) { requiredChildren.push(fieldId); } 194 | repeaterChildren.push(fieldId); 195 | }); 196 | 197 | repeaterField['repeaterId'] = repeaterId; 198 | repeaterField['repeaterRequiredChildren'] = requiredChildren; 199 | repeaterField['repeaterChildren'] = repeaterChildren; 200 | }); 201 | } 202 | 203 | function gfRepeater_editforms_updateRequired(field, leaving) { 204 | var fieldId = gfRepeater_getId(field.id); 205 | var getField = gfRepeater_getField(field.id); 206 | var gfRequired = getField['isRequired']; 207 | var repeaterRequired = getField['repeaterField_isRequired']; 208 | var returnRequired = false; 209 | 210 | if (gfRepeater_debug) { console.log(field.id+' - Before Filter - isRequired:'+getField['isRequired']+' - repeaterField_isRequired:'+getField['repeaterField_isRequired']); } 211 | 212 | if (gfRequired && leaving) { 213 | getField['isRequired'] = false; 214 | getField['repeaterField_isRequired'] = true; 215 | if (gfRepeater_debug) { console.log(field.id+' - Used Filter 1'); } 216 | returnRequired = true; 217 | } 218 | 219 | if (!gfRequired && leaving) { 220 | getField['repeaterField_isRequired'] = false; 221 | if (gfRepeater_debug) { console.log(field.id+' - Used Filter 2'); } 222 | } 223 | 224 | if (repeaterRequired && !leaving) { 225 | jQuery(field).find('.gfield_required').html('*'); 226 | getField['isRequired'] = true; 227 | getField['repeaterField_isRequired'] = false; 228 | if (gfRepeater_debug) { console.log(field.id+' - Used Filter 3'); } 229 | } 230 | 231 | if (!repeaterRequired && !leaving) { 232 | jQuery(field).find('.gfield_required').html(''); 233 | getField['isRequired'] = false; 234 | if (gfRepeater_debug) { console.log(field.id+' - Used Filter 4'); } 235 | } 236 | 237 | if (gfRepeater_debug) { console.log(field.id+' - After Filter - isRequired:'+getField['isRequired']+' - repeaterField_isRequired:'+getField['repeaterField_isRequired']); } 238 | 239 | return returnRequired; 240 | } 241 | 242 | function gfRepeater_editforms_fieldCheck(type, index) { 243 | var supportedTypes = ['text', 'textarea', 'select', 'multiselect', 'number', 244 | 'checkbox', 'radio', 'hidden', 'html', 'section', 245 | 'name', 'date', 'time', 'phone', 'address', 246 | 'website', 'email', 'product', 'quantity', 'option', 247 | 'shipping', 'total']; 248 | 249 | if (gfRepeater_isIndexInsideRepeater(index)) { 250 | if (type == 'repeater') { 251 | alert('Nesting repeaters is not supported!'); 252 | return false; 253 | } else if (type !== 'repeater-end' && jQuery.inArray(type, supportedTypes) == -1) { 254 | alert('Sorry, the ' + type + ' field is not supported by the Gravity Forms Repeater Add-On at this time. :('); 255 | return false; 256 | } 257 | } 258 | 259 | return true; 260 | } 261 | 262 | function gfRepeater_editforms_patch_SaveForm() { 263 | var og_SaveForm = SaveForm; 264 | 265 | SaveForm = function(isNew) { 266 | if (gfRepeater_brokenRepeater()) { 267 | return false; 268 | } 269 | 270 | gfRepeater_editforms_update(true); 271 | 272 | return og_SaveForm.call(this, isNew); 273 | } 274 | } 275 | 276 | function gfRepeater_editforms_patch_StartAddField() { 277 | var og_StartAddField = StartAddField; 278 | 279 | StartAddField = function(type, index) { 280 | var keepGoing = gfRepeater_editforms_fieldCheck(type, index); 281 | 282 | if (!keepGoing) { 283 | jQuery('#gform_adding_field_spinner').remove(); 284 | return; 285 | } 286 | 287 | return og_StartAddField.call(this, type, index); 288 | }; 289 | } 290 | 291 | function gfRepeater_editforms_patch_EndAddField() { 292 | var og_EndAddField = EndAddField; 293 | 294 | EndAddField = function(field, fieldString, index) { 295 | og_EndAddField.call(this, field, fieldString, index); 296 | 297 | if (field.type == 'repeater') { 298 | StartAddField('repeater-end', index + 1); 299 | } 300 | }; 301 | } 302 | 303 | function gfRepeater_editforms_patch_StartDeleteField() { 304 | var og_StartDeleteField = StartDeleteField; 305 | 306 | StartDeleteField = function(element) { 307 | var fieldId = jQuery(element)[0].id.split("_")[2]; 308 | var field = GetFieldById(fieldId); 309 | 310 | if (field.type == 'repeater-end') { 311 | alert('This field cannot be deleted directly. Please delete the repeater start field first.'); 312 | return; 313 | } 314 | 315 | og_StartDeleteField.call(this, element); 316 | }; 317 | } 318 | 319 | function gfRepeater_editforms_patch_EndDeleteField() { 320 | var og_EndDeleteField = EndDeleteField; 321 | 322 | EndDeleteField = function(fieldId) { 323 | var field = GetFieldById(fieldId); 324 | 325 | if (field.type == 'repeater') { 326 | var repeaterEndId = gfRepeater_getRepeaterEndId(fieldId); 327 | } 328 | 329 | og_EndDeleteField.call(this, fieldId); 330 | 331 | if (field.type == 'repeater' && repeaterEndId) { 332 | repeaterEndId = gfRepeater_getId(repeaterEndId); 333 | DeleteField(repeaterEndId); 334 | } 335 | }; 336 | } 337 | 338 | function gfRepeater_editforms_patch_SortUpdate() { 339 | jQuery('#gform_fields').sortable({ 340 | update: function (event, ui) { 341 | if (gfRepeater_brokenRepeater()) { 342 | jQuery('#gform_fields').sortable('cancel'); 343 | } 344 | } 345 | }); 346 | } 347 | 348 | jQuery(document).ready(function($) { 349 | if (gfRepeater_page == 'gf_edit_forms') { 350 | gfRepeater_editforms_update(false); 351 | } 352 | }); 353 | 354 | jQuery(window).load(function() { 355 | if (gfRepeater_page == 'gf_edit_forms') { 356 | gfRepeater_editforms_patch_SaveForm(); 357 | gfRepeater_editforms_patch_StartAddField(); 358 | gfRepeater_editforms_patch_EndAddField(); 359 | gfRepeater_editforms_patch_StartDeleteField(); 360 | gfRepeater_editforms_patch_EndDeleteField(); 361 | gfRepeater_editforms_patch_SortUpdate(); 362 | } 363 | }); 364 | 365 | // Debug shortcuts 366 | if (gfRepeater_debug) { 367 | jQuery(window).keydown(function(event){ 368 | // Up Arrow - Prints the contents of gfRepeater_repeaters into the console. 369 | if (event.which == 38) { console.log(gfRepeater_repeaters); } 370 | 371 | // Down Arrow - Loop through all of the fields (even non-repeated fields) and output their settings to the console. 372 | if (event.which == 40) { 373 | jQuery('.gfield').each(function(){ 374 | console.log(gfRepeater_getField(this.id)); 375 | }); 376 | } 377 | }); 378 | } 379 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WAIT! 2 | **This plugin is no longer maintained and is probably broken, go use [Gravity Forms Nested Forms by Gravity Wiz](https://gravitywiz.com/documentation/gravity-forms-nested-forms/?ref=145) instead!** 3 | 4 | ![](/assets/icon-256x256.png?raw=true) 5 | # Gravity Forms Repeater Add-On 6 | A Gravity Forms add-on that allows specified groups of fields to be repeated by the user. 7 | 8 | 9 | ### Supported Fields 10 | * Address 11 | * Checkboxes 12 | * Date 13 | * Drop Down 14 | * Email 15 | * Hidden 16 | * HTML 17 | * MultiSelect 18 | * Name 19 | * Number 20 | * Paragraph Text 21 | * Phone 22 | * Radio 23 | * Section 24 | * Single Line Text 25 | * Time 26 | * Website 27 | 28 | ### Features 29 | * Repeat groups of fields multiple times 30 | * Use multiple repeaters on the same form 31 | * Use shortcodes to display data to the user 32 | * Use Javascript to manipulate the repeater 33 | * Customize the add and remove button's HTML 34 | * Use Gravity Forms pre-populate hooks and filters like normal 35 | * Supports Conditional Logic! 36 | 37 | ### Issues 38 | * Not all fields are currently supported. 39 | * Ajax enabled forms are not yet supported. (Ajax will be automatically disabled on forms with repeaters) 40 | 41 | ### Shortcodes 42 | You can place shortcodes inside of input labels, input descriptions, and HTML blocks! 43 | * [gfRepeater-count] - Will output the current repeat number. 44 | * [gfRepeater-buttons] - Will output both the '+' and '-' buttons. 45 | * [gfRepeater-add] - Will output the '+' button. 46 | * [gfRepeater-remove] - Will output the '-' button. 47 | 48 | ### CSS Classes 49 | You can use these CSS classes in the in the "Custom CSS Class" setting to do different things. 50 | * gf_repeater_hide_add - Will hide the '+' button if placed in the repeater end css setting. 51 | * gf_repeater_hide_remove - Will hide the '-' button if placed in the repeater end css setting. 52 | 53 | ### Javascript 54 | ##### Functions 55 | You can use Javascript to manipulate the repeater. 56 | * formId is your form's Id number assigned by Gravity Forms. 57 | * repeaterId will depened on how many repeaters you have in your form. (The first repeater's ID is 1, second is 2, etc.) 58 | * repeaterChildId works the same way and depends on how many times the repeater has been repeated. 59 | * These functions will not allow you to repeat more than the set max and unrepeat more than the set min. 60 | # 61 | ```javascript 62 | // Repeats the repeater once. 63 | gfRepeater_repeatRepeater(formId, repeaterId); 64 | ``` 65 | ```javascript 66 | // Un-repeats the repeater once. repeaterChildId is optional. 67 | gfRepeater_unrepeatRepeater(formId, repeaterId, repeaterChildId); 68 | ``` 69 | ```javascript 70 | // Repeats the repeater a number of times. 71 | gfRepeater_repeatRepeaterTimes(formId, repeaterId, timesX); 72 | ``` 73 | ```javascript 74 | // UnRepeats the repeater a number of times. 75 | gfRepeater_unrepeatRepeaterTimes(formId, repeaterId, timesX); 76 | ``` 77 | ```javascript 78 | // Repeats or unrepeats the repeater to get it to whatever timesX is. 79 | gfRepeater_setRepeater(formId, repeaterId, timesX); 80 | ``` 81 | ```javascript 82 | // Selects an element depending on the variables passed. All variables are optional besides formId. inputId also accepts '*' to select all inputs. 83 | gfRepeater_select(formId, repeaterId, repeatId, childId, inputId); 84 | ``` 85 | 86 | ##### Triggers 87 | These triggers are assigned to the form will be fired during different repeater related events with the repeaterId and repeaterChildId attached to them. 88 | ``` 89 | gform_repeater_before_repeat - Fires right before a repeater is about to be repeated. 90 | gform_repeater_after_repeat - Fires right after a repeater has been repeated. 91 | gform_repeater_before_unrepeat - Fires right before a repeater is about to be un-repeated. 92 | gform_repeater_after_unrepeat - Fires right after a repeater has been un-repeated. 93 | gform_repeater_init_done - Fires after the repeater is done setting up. Also fires on the window when all repeaters are done setting up. 94 | ``` 95 | 96 | ##### Information 97 | You can access all repeater information using the object `gfRepeater_repeaters`. 98 | ``` 99 | gfRepeater_repeaters 100 | [formId] 101 | [repeaterId] 102 | ['data'] - Contains different data to keep track of. 103 | ['repeatCount'] - The number of times the repeater has been repeated. 104 | ['prevRepeatCount'] - If the form has already been submitted and failed validation, this will contain the repeatCount from before the from was submitted, otherwise it will be null. 105 | ['childrenCount'] - The number of children fields that get cloned everytime the repeater is repeated. 106 | ['paremCount'] - The highest field ID that has had their prepopulate paremeter set. 107 | ['tabIndex'] - The tabindex that was assigned to the repeater start field and will be assigned to all children. 108 | ['inputData'] - Contains an array with all of the input names in the repeater. 109 | ['startIdNum'] - ID Number of the repeater start field. 110 | ['endIdNum'] - ID Number of the repeater end field. 111 | ['settings'] - Contains the different settings that are set in the form editor. 112 | ['start'] - The number of times the repeater should be repeated when the form is loaded. 113 | ['min'] - The minimum number of times the repeater can be repeated. 114 | ['max'] - The maximum number of times the repeater can be repeated. 115 | ['controllers'] - Contains different elements that I like to call controllers. 116 | ['add'] - The add button. 117 | ['remove'] - The remove button. 118 | ['data'] - The hidden element that stores data to be passed to PHP. 119 | ['start'] - The start of the repeater. 120 | ['end'] - The end of the repeater. 121 | ['children'] - Contains the children fields that get cloned everytime the repeater is repeated. 122 | [childId] 123 | ['element'] - The field element. 124 | ['id'] - The field HTML id. 125 | ['idNum'] - The field ID number. 126 | ['inputCount'] - The number of inputs the child contains. 127 | ['inputMask'] - The field's input mask. 128 | ['required'] - True or False depeneding on if the field is required or not. 129 | ['type'] - The field type. 130 | ['inputs'] - Contains all of the inputs that the child contains. 131 | [inputId] 132 | ['element'] - The input element. 133 | ['id'] - The input HTML id. 134 | ['name'] - The input name. 135 | ['defaultValue'] - The default value for the input. 136 | ['prePopulate'] - Array containing prepopulate values. 137 | ``` 138 | 139 | ##### Usage Examples 140 | Repeat the repeater a number of times depending on the value of a drop down field: 141 | ```javascript 142 | jQuery('#gform_6 #input_6_7').change(function(){ 143 | var attendees = jQuery(this).val(); 144 | gfRepeater_setRepeater(6, 1, attendees); 145 | }); 146 | ``` 147 | 148 | Change the value of a field if the repeater is repeated or un-repeated: 149 | ```javascript 150 | jQuery('#gform_9').on('gform_repeater_after_repeat gform_repeater_after_unrepeat', function(event, repeaterId, repeatId){ 151 | if (repeaterId == 1) { 152 | var repeatCount = gfRepeater_repeaters[9][1]['data']['repeatCount']; 153 | var totalPrice = 27.47 * repeatCount; 154 | jQuery('#gform_9 #input_9_4').val('$'+totalPrice); 155 | } 156 | }); 157 | ``` 158 | 159 | 160 | ### Prepopulate Fields 161 | You can set Parameter Names to prepopulate repeated fields like usual with the added ability to specify which repeated set will be prepopulated. For example, let's say we have set the parameter name for one of our repeated fields to "parem": 162 | 163 | `?parem=hello+world` will result in that field being set to "hello world" regardless of how many times it has been repeated. 164 | 165 | `?parem3=hello+world` will result in that field being set to "hello world" only when repeated a third time. 166 | 167 | You can use filters as well! 168 | 169 | ``` 170 | add_filter( 'gform_field_value_parem', 'your_function_name' ); 171 | ``` 172 | 173 | ``` 174 | add_filter( 'gform_field_value_parem3', 'your_function_name' ); 175 | ``` 176 | 177 | Also, setting the prepopulate parameter on the repeater start field will override the `start` setting. 178 | 179 | 180 | ### Frequently Asked Questions 181 | ##### Can I use multiple repeaters in one form? 182 | Yes! 183 | 184 | ##### Can I nest repeaters? 185 | Unfortunately nesting repeaters is not supported at this time. 186 | 187 | ##### Can I change the `+` and `-` buttons to text links? 188 | Yes! Just go to the form editor and change the `Add HTML` and `Remove HTML` settings to `Your Link Text` and they should appear as regular links on your form! 189 | 190 | ### Development 191 | 192 | ##### Method 1 (Automatic Updates) 193 | It is possible to use the development version of this plugin in your Wordpress install and keep it updated with [Andy Fragen](https://github.com/afragen)'s [GitHub Updater](https://github.com/afragen/github-updater)! 194 | 195 | 1. Download & Install [GitHub Updater](https://github.com/afragen/github-updater). 196 | 1. Go to Settings -> GitHub Updater. 197 | 1. Enable Branch Switching. 198 | 1. Go to Plugins. 199 | 1. Select "try another branch" under Gravity Forms Repeater Add-On. 200 | 1. Select "development". 201 | 202 | and that's it! You will recieve the latest development versions of this plugin! 203 | 204 | ##### Method 2 (Manual) 205 | 206 | 1. Deactivate & Uninstall current version of Gravity Forms Repeater Add-On. 207 | 1. Download the [latest development version](https://github.com/kodie/gravityforms-repeater/archive/development.zip). 208 | 1. Go to Plugins. 209 | 1. Select "Add New". 210 | 1. Select "Upload Plugin". 211 | 1. Select "Choose File" and find the zip file that you downloaded in step 2. 212 | 1. Select "Install Now". 213 | 214 | and you're good to go! 215 | 216 | **Warning: There is a slight possibility that development versions are unstable. Use at your own risk.** 217 | 218 | ### Version 219 | 1.0.9 220 | 221 | ### Changelog 222 | ##### 1.0.9 223 | * Fixed bug where in some cases a PHP error would occur when attempting to automatically disable ajax on a form. 224 | * Fixed bug where sometimes a javascript error would occur on date fields with a calendar icon. 225 | * Fixed bug where removing a specific repeatId would cause some weird things to happen. 226 | * Fixed bug where repeaters in a multipage form that are not on the first page would appear to have failed validation even if the form hasn't been submitted. 227 | * Fixed bug where repeaters in a muttipage form that are not on the first page would forget their saved data and ignore their start setting. 228 | * Fixed hideButtons setting. 229 | * Added [gfRepeater-buttons], [gfRepeater-add], and [gfRepeater-remove] shortcodes. 230 | * Added custom CSS option to repeater start and end fields. 231 | * Added gf_repeater_hide_add and gf_repeater_hide_remove CSS classes. 232 | * Added gfRepeater_select javascript function. 233 | * Added 'gform_repeater_init_done' trigger to window and individual forms. 234 | * Changed 'beforeRepeat', 'afterRepeat', 'beforeUnRepeat', and 'afterUnRepeat' triggers to 'gform_repeater_before_repeat', 'gform_repeater_after_repeat', 'gform_repeater_before_unrepeat', and 'gform_repeater_after_unrepeat'. 235 | * Repeated fields now respect conditional logic from outside the repeater. 236 | * Applying conditional logic on a repeated section field now also applys that logic on repeated fields under that section. 237 | * When exporting entries, repeater child data is now exported in a comma-separated list. 238 | 239 | ### Requirements 240 | * Wordpress 3.9 or later 241 | * Gravity Forms 1.9 or later 242 | 243 | ### Installation 244 | 1. Upload the `repeater-add-on-for-gravity-forms` folder to the `/wp-content/plugins/` directory. 245 | 1. Activate the plugin through the 'Plugins' menu in WordPress. 246 | -------------------------------------------------------------------------------- /js/jquery.postcapture.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery Post Capture - v0.0.2 - 3/31/2017 2 | * https://github.com/robertstaddon/jQuery-PostCapture 3 | * Copyright (c) 2014 ssut (SuHun Han); Licensed BSD 3-Clause */ 4 | 5 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a(window.jQuery)}(function(a){function c(a){return h.raw?a:encodeURIComponent(a)}function d(a){return h.raw?a:decodeURIComponent(a)}function e(a){return c(h.json?JSON.stringify(a):String(a))}function f(a){0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{return a=decodeURIComponent(a.replace(b," ")),h.json?JSON.parse(a):a}catch(a){}}function g(b,c){var d=h.raw?b:f(b);return a.isFunction(c)?c(d):d}function r(a){("/"===a.slice(0,1)||"http:"!==a.slice(0,5)&&"https:"!==a.slice(0,6))&&(current=r(location.href),a=current.protocol+"//"+current.host+a);var b=a.match(/^(https?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)(\/?[^?#]*)(\?[^#]*|)(#.*|)$/);return b&&{protocol:b[1],host:b[2],hostname:b[3],port:b[4],pathname:b[5],search:b[6],hash:b[7]}}var b=/\+/g,h=a.cookie=function(b,f,i){if(void 0!==f&&!a.isFunction(f)){if(i=a.extend({},h.defaults,i),"number"==typeof i.expires){var j=i.expires,k=i.expires=new Date;k.setTime(+k+864e5*j)}return document.cookie=[c(b),"=",e(f),i.expires?"; expires="+i.expires.toUTCString():"",i.path?"; path="+i.path:"",i.domain?"; domain="+i.domain:"",i.secure?"; secure":""].join("")}for(var l=b?void 0:{},m=document.cookie?document.cookie.split("; "):[],n=0,o=m.length;n-1?(c.hasOwnProperty(h)||(c[h]=[]),f.is(":checked")&&c[h].push(m)):f.is(":checked")&&(c[h]="on");else if("radio"==j)f.is(":checked")&&(""!==m?c[h]=m:c[h]="on");else if("file"==j){if(c.hasOwnProperty(h)||(c[h]=[]),""!==m)if(void 0!==g.files)for(var n=0;n-1?(c.hasOwnProperty(h)||(c[h]=[]),c[h].push(m)):c[h]=m}var v="formCapture#";for(e=0;e GF_REPEATER_DEBUG)); 37 | 38 | wp_localize_script('gforms_repeater_js', 'GF_Repeater_Phrases', 39 | array( 40 | 'field_required' => __( 'This field is required.', 'gravityformsrepeater' ), 41 | ) 42 | ); 43 | 44 | wp_enqueue_script('gforms_repeater_postcapture_js'); 45 | wp_enqueue_script('gforms_repeater_js'); 46 | wp_enqueue_style('gforms_repeater_css'); 47 | } 48 | } 49 | } 50 | 51 | public function get_form_editor_field_title() { 52 | return 'Repeater'; 53 | } 54 | 55 | public function get_form_editor_field_settings() { 56 | return apply_filters('gform_editor_repeater_field_settings', 57 | array( 58 | 'admin_label_setting', 59 | 'css_class_setting', 60 | 'description_setting', 61 | 'error_message_setting', 62 | 'label_setting', 63 | 'prepopulate_field_setting', 64 | 'conditional_logic_field_setting' 65 | ) 66 | ); 67 | } 68 | 69 | public function add_button($field_groups) { 70 | foreach ($field_groups as &$group) { 71 | if ($group['name'] == 'advanced_fields') { 72 | $group['fields'][] = array( 73 | 'class' => 'button', 74 | 'data-type' => 'repeater', 75 | 'value' => 'Repeater', 76 | 'onclick' => "StartAddField('repeater');" 77 | ); 78 | break; 79 | } 80 | } 81 | 82 | return $field_groups; 83 | } 84 | 85 | public static function gform_set_defaults() { 86 | echo " 87 | case \"repeater\" : 88 | field.label = \"Repeater\"; 89 | break; 90 | "; 91 | } 92 | 93 | public static function gform_standard_settings($position, $form_id) { 94 | if ($position == 1600) { 95 | echo "
  • 96 | 101 | 102 |
  • "; 103 | 104 | echo "
  • 105 | 110 | 111 |
  • "; 112 | 113 | echo "
  • 114 | 119 | 120 |
  • "; 121 | } 122 | } 123 | 124 | public static function gform_appearance_settings($position, $form_id) { 125 | if ($position == 400) { 126 | echo "
  • 127 | 132 | 133 |
  • "; 134 | 135 | echo "
  • 136 | 137 | 142 |
  • "; 143 | } 144 | } 145 | 146 | public static function gform_editor() { 147 | echo ""; 157 | } 158 | 159 | public static function gform_tooltips($tooltips) { 160 | $tooltips['form_field_repeater_start'] = __( "The number of times the repeater will be repeated when the form is rendered. Leaving this field blank or setting it to a number higher than the maximum number is the same as setting it to 1.", 'gravityformsrepeater' ); 161 | $tooltips['form_field_repeater_min'] = __( "The minimum number of times the repeater is allowed to be repeated. Leaving this field blank or setting it to a number higher than the maximum field is the same as setting it to 1.", 'gravityformsrepeater' ); 162 | $tooltips['form_field_repeater_max'] = __( "The maximum number of times the repeater is allowed to be repeated. Leaving this field blank or setting it to a number lower than the minimum field is the same as setting it to unlimited.", 'gravityformsrepeater' ); 163 | $tooltips['form_field_repeater_animations'] = __( "A JavaScript object to be passed for animation settings. For advanced users only. Do not include initial brackets.", 'gravityformsrepeater' ); 164 | $tooltips['form_field_repeater_hideLabel'] = __( "If this is checked, the repeater label and description will not be shown to users on the form.", 'gravityformsrepeater' ); 165 | return $tooltips; 166 | } 167 | 168 | function validate($value, $form) { 169 | $repeater_required = $this->repeaterRequiredChildren; 170 | 171 | if (!empty($repeater_required)) { 172 | $dataArray = json_decode($value, true); 173 | 174 | foreach ($form['fields'] as $key=>$value) { 175 | $fieldKeys[$value['id']] = $key; 176 | 177 | if (is_array($value['inputs'])) { 178 | foreach ($value['inputs'] as $inputKey=>$inputValue) { 179 | $inputKeys[$value['id']][$inputValue['id']] = $inputKey; 180 | } 181 | } 182 | } 183 | 184 | if ($dataArray['repeatCount'] < $this->min) { 185 | $this->failed_validation = true; 186 | $this->validation_message = sprintf( __( "A minimum number of %s is required.", 'gravityformsrepeater' ), $this->min ); 187 | return; 188 | } 189 | 190 | if ($this->max && $dataArray['repeatCount'] > $this->max) { 191 | $this->failed_validation = true; 192 | $this->validation_message = sprintf( __( "A maximum number of %s is allowed.", 'gravityformsrepeater' ), $this->max ); 193 | return; 194 | } 195 | 196 | for ($i = 1; $i < $dataArray['repeatCount'] + 1; $i++) { 197 | foreach ($dataArray['children'] as $field_id=>$field) { 198 | $repeatSkips = array(); 199 | 200 | if (array_key_exists('conditionalLogic', $field)) { 201 | if (is_array($field['conditionalLogic'])) { 202 | if (array_key_exists('skip', $field['conditionalLogic'])) { 203 | $repeatSkips = $field['conditionalLogic']['skip']; 204 | } 205 | } 206 | } 207 | 208 | if (array_key_exists('inputs', $field)) { 209 | $inputNames = $field['inputs']; 210 | } else { continue; } 211 | 212 | if (is_array($repeatSkips)) { 213 | if (in_array($i, $repeatSkips) || in_array('all', $repeatSkips)) { continue; } 214 | } 215 | 216 | foreach ($inputNames as $inputName) { 217 | if (is_array($inputName)) { $inputName = reset($inputName); } 218 | 219 | if (substr($inputName, -2) == '[]') { 220 | $getInputName = substr($inputName, 0, strlen($inputName) - 2).'-'.$dataArray['repeaterId'].'-'.$i; 221 | } else { 222 | $getInputName = $inputName.'-'.$dataArray['repeaterId'].'-'.$i; 223 | } 224 | 225 | $getInputName = str_replace('.', '_', strval($getInputName)); 226 | $getInputData = rgpost($getInputName); 227 | $getInputIdNum = preg_split("/(_|-)/", $getInputName); 228 | 229 | if (in_array($getInputIdNum[1], $repeater_required)) { 230 | $fieldKey = $fieldKeys[$getInputIdNum[1]]; 231 | $fieldType = $form['fields'][$fieldKey]['type']; 232 | $failedValidation = false; 233 | 234 | switch($fieldType) { 235 | case 'name': 236 | $requiredIDs = array(3, 6); 237 | if (in_array($getInputIdNum[2], $requiredIDs) && empty($getInputData)) { $failedValidation = true; } 238 | break; 239 | case 'address': 240 | $skipIDs = array(2); 241 | if (!in_array($getInputIdNum[2], $skipIDs) && empty($getInputData)) { $failedValidation = true; } 242 | break; 243 | default: 244 | if (empty($getInputData)) { $failedValidation = true; } 245 | } 246 | 247 | if ($failedValidation) { 248 | $this->failed_validation = true; 249 | if ($this->errorMessage) { $this->validation_message = $this->errorMessage; } else { $this->validation_message = __( "A required field was left blank.", 'gravityformsrepeater' ); } 250 | return; 251 | } 252 | } 253 | } 254 | } 255 | } 256 | } 257 | } 258 | 259 | public function get_field_content($value, $force_frontend_label, $form) { 260 | if (is_admin()) { 261 | $admin_buttons = $this->get_admin_buttons(); 262 | $field_content = "{$admin_buttons} 263 |
    264 |
    begin repeater
    265 |
    REPEATER
    266 |
    top of repeater
    267 |
    "; 268 | } else { 269 | $field_label = $this->get_field_label($force_frontend_label, $value); 270 | $description = $this->get_description($this->description, 'gsection_description gf_repeater_description'); 271 | $hide_label = $this->hideLabel; 272 | $validation_message = ( $this->failed_validation && ! empty( $this->validation_message ) ) ? sprintf( "
    %s
    ", $this->validation_message ) : ''; 273 | if (!empty($field_label)) { $field_label = "

    {$field_label}

    "; } else { $field_label = ''; } 274 | if ($hide_label) { $field_label = ''; $description = ''; } 275 | $field_content = "
    {$field_label}{FIELD}
    {$description}{$validation_message}"; 276 | } 277 | return $field_content; 278 | } 279 | 280 | public function get_field_input($form, $value = '', $entry = null) { 281 | if (is_admin()) { 282 | return ''; 283 | } else { 284 | $form_id = $form['id']; 285 | $is_entry_detail = $this->is_entry_detail(); 286 | $is_form_editor = $this->is_form_editor(); 287 | $id = (int) $this->id; 288 | $field_id = $is_entry_detail || $is_form_editor || $form_id == 0 ? "input_$id" : 'input_' . $form_id . "_$id"; 289 | $tabindex = $this->get_tabindex(); 290 | $repeater_id = $this->repeaterId; 291 | $repeater_parem = $this->inputName; 292 | $repeater_required = $this->repeaterRequiredChildren; 293 | $repeater_children = $this->repeaterChildren; 294 | $repeater_start = apply_filters('gf_repeater_start', $this->start, $form, $this); 295 | $repeater_min = apply_filters('gf_repeater_min', $this->min, $form, $this); 296 | $repeater_max = apply_filters('gf_repeater_max', $this->max, $form, $this); 297 | $repeater_animations = apply_filters('gf_repeater_animations', $this->animations, $form, $this); 298 | 299 | if (!empty($repeater_parem)) { 300 | $repeater_parem_value = GFFormsModel::get_parameter_value($repeater_parem, $value, $this); 301 | if (!empty($repeater_parem_value)) { $repeater_start = $repeater_parem_value; } 302 | } 303 | 304 | if (!empty($repeater_children)) { 305 | $repeater_children_info = array(); 306 | $repeater_parems = GF_Field_Repeater::get_children_parem_values($form, $repeater_children); 307 | 308 | foreach($repeater_children as $repeater_child) { 309 | $repeater_children_info[$repeater_child] = array(); 310 | $repeater_child_field_index = GF_Field_Repeater::get_field_index($form, 'id', $repeater_child); 311 | 312 | if (!empty($repeater_required)) { 313 | if (in_array($repeater_child, $repeater_required)) { 314 | $repeater_children_info[$repeater_child]['required'] = true; 315 | } 316 | } 317 | 318 | if (!empty($repeater_parems)) { 319 | if (array_key_exists($repeater_child, $repeater_parems)) { 320 | $repeater_children_info[$repeater_child]['prePopulate'] = $repeater_parems[$repeater_child]; 321 | } 322 | } 323 | 324 | if ($repeater_child_field_index !== false) { 325 | $repeater_child_field = $form['fields'][$repeater_child_field_index]; 326 | 327 | if ($repeater_child_field['inputMask']) { 328 | $repeater_children_info[$repeater_child]['inputMask'] = $repeater_child_field['inputMaskValue']; 329 | } elseif ($repeater_child_field['type'] == 'phone' && $repeater_child_field['phoneFormat'] = 'standard') { 330 | $repeater_children_info[$repeater_child]['inputMask'] = "(999) 999-9999"; 331 | } 332 | 333 | if ($repeater_child_field['conditionalLogic']) { 334 | $repeater_children_info[$repeater_child]['conditionalLogic'] = $repeater_child_field['conditionalLogic']; 335 | } 336 | 337 | if ($repeater_child_field['maxLength']) { 338 | $repeater_children_info[$repeater_child]['maxLength'] = $repeater_child_field['maxLength']; 339 | } 340 | 341 | if ($repeater_child_field['enableEnhancedUI']) { 342 | $repeater_children_info[$repeater_child]['enableEnhancedUI'] = $repeater_child_field['enableEnhancedUI']; 343 | } 344 | } 345 | } 346 | 347 | $repeater_children = $repeater_children_info; 348 | } 349 | 350 | $value = array(); 351 | $value['formId'] = $form_id; 352 | if (!empty($repeater_start)) { $value['start'] = $repeater_start; } 353 | if (!empty($repeater_min)) { $value['min'] = $repeater_min; } 354 | if (!empty($repeater_max)) { $value['max'] = $repeater_max; } 355 | if (!empty($repeater_children)) { $value['children'] = $repeater_children; } 356 | $value = json_encode($value); 357 | 358 | if (!empty($repeater_animations)) { 359 | $animation_script = "var animations={".$repeater_animations."};jQuery.extend(true,gfRepeater_repeaters[".$form_id."][".$repeater_id."].settings.animations,animations);"; 360 | GFFormDisplay::add_init_script($form_id, 'repeater_animations', GFFormDisplay::ON_PAGE_RENDER, $animation_script); 361 | } 362 | 363 | return sprintf("", $id, $field_id, $value, $tabindex); 364 | } 365 | } 366 | 367 | public function get_value_save_entry($value, $form, $input_name, $lead_id, $lead) { 368 | $dataArray = json_decode($value, true); 369 | $value = Array(); 370 | 371 | for ($i = 1; $i < $dataArray['repeatCount'] + 1; $i++) { 372 | $childValue = Array(); 373 | if( !array_key_exists('children', $dataArray) && !isset($dataArray['children']) ){ 374 | continue; 375 | } 376 | foreach ($dataArray['children'] as $field_id=>$field) { 377 | $inputData = Array(); 378 | 379 | if (array_key_exists('inputs', $field)) { 380 | $inputNames = $field['inputs']; 381 | $repeatSkips = array(); 382 | 383 | if (array_key_exists('conditionalLogic', $field)) { 384 | if (is_array($field['conditionalLogic'])) { 385 | if (array_key_exists('skip', $field['conditionalLogic'])) { 386 | $repeatSkips = $field['conditionalLogic']['skip']; 387 | } 388 | } 389 | } 390 | 391 | if (is_array($repeatSkips)) { 392 | if (in_array($i, $repeatSkips) || in_array('all', $repeatSkips)) { continue; } 393 | } 394 | 395 | if (is_array($inputNames)) { 396 | foreach ($inputNames as $inputName) { 397 | if (substr($inputName, -2) == '[]') { 398 | $getInputName = substr($inputName, 0, strlen($inputName) - 2).'-'.$dataArray['repeaterId'].'-'.$i; 399 | } else { 400 | $getInputName = $inputName.'-'.$dataArray['repeaterId'].'-'.$i; 401 | } 402 | 403 | $input_field_id_num = explode('.', $inputName); 404 | if (count($input_field_id_num) == 2) { $input_field_id_num = $input_field_id_num[1]; } else { $input_field_id_num = null; } 405 | 406 | $getInputData = rgpost(str_replace('.', '_', strval($getInputName))); 407 | 408 | if (!empty($getInputData)) { 409 | if (is_array($getInputData)) { 410 | $inputCount = 0; 411 | foreach ($getInputData as $theInputData) { 412 | if (!$input_field_id_num) { 413 | $inputCount++; 414 | $inputData[$inputCount] = $theInputData; 415 | } else { 416 | $inputData[$input_field_id_num] = $theInputData; 417 | } 418 | } 419 | } else { 420 | $inputData[1] = $getInputData; 421 | } 422 | } 423 | } 424 | } 425 | } else { 426 | if (GF_Field_Repeater::get_field_type($form, $field_id) == 'section') { $inputData = '[gfRepeater-section]'; } 427 | } 428 | 429 | $childValue[$field_id] = $inputData; 430 | } 431 | $value[$i] = $childValue; 432 | } 433 | 434 | return maybe_serialize($value); 435 | } 436 | 437 | public function get_value_entry_list($value, $entry, $field_id, $columns, $form) { 438 | if (empty($value)) { 439 | return ''; 440 | } else { 441 | $dataArray = GFFormsModel::unserialize($value); 442 | $arrayCount = count($dataArray); 443 | if ($arrayCount > 1) { $returnText = $arrayCount.' entries'; } else { $returnText = $arrayCount.' entry'; } 444 | return $returnText; 445 | } 446 | } 447 | 448 | public function get_value_entry_detail($value, $currency = '', $use_text = false, $format = 'html', $media = 'screen') { 449 | if (empty($value)) { 450 | return ''; 451 | } else { 452 | $dataArray = GFFormsModel::unserialize($value); 453 | $arrayCount = count($dataArray); 454 | $output = "\n"; 455 | $count = 0; 456 | $repeatCount = 0; 457 | $display_empty_fields = rgget('gf_display_empty_fields', $_COOKIE); 458 | $form_id = $this->formId; 459 | $get_form = GFFormsModel::get_form_meta_by_id($form_id); 460 | $form = $get_form[0]; 461 | 462 | foreach ($dataArray as $key=>$value) { 463 | $repeatCount++; 464 | $tableContents = ''; 465 | 466 | if (empty($value)) { continue; } 467 | 468 | if (!empty($value) && !is_array($value)) { 469 | $save_value = $value; 470 | unset($value); 471 | $value[0] = $save_value; 472 | } elseif (version_compare(phpversion(), '5.3') !== -1) { 473 | uksort($value, function($a, $b) use($form){ 474 | $a_index = GF_Field_Repeater::get_field_index($form, 'id', $a); 475 | $b_index = GF_Field_Repeater::get_field_index($form, 'id', $b); 476 | if ($a_index > $b_index) { return 1; } 477 | return 0; 478 | }); 479 | } 480 | 481 | foreach ($value as $childKey=>$childValue) { 482 | $count++; 483 | $childValueOutput = ''; 484 | 485 | if (empty($display_empty_fields) && count($childValue) == 0) { continue; } 486 | 487 | if (is_numeric($childKey)) { 488 | $field_index = GF_Field_Repeater::get_field_index($form, 'id', $childKey); 489 | if ($field_index === false) { continue; } 490 | $entry_title = $form['fields'][$field_index]['label']; 491 | } else { 492 | $entry_title = $childKey; 493 | } 494 | 495 | $entry_title = str_replace('[gfRepeater-count]', $repeatCount, $entry_title); 496 | 497 | if ($format == 'html') { 498 | if ($childValue == '[gfRepeater-section]') { 499 | if ($media == 'email') { 500 | $tableStyling = ' style="font-size:14px;font-weight:bold;background-color:#eee;border-bottom:1px solid #dfdfdf;padding:7px 7px"'; 501 | } else { 502 | $tableStyling = ' class="entry-view-section-break"'; 503 | } 504 | } else { 505 | if ($media == 'email') { 506 | $tableStyling = ' style="background-color:#EAF2FA;font-family:sans-serif;font-size:12px;font-weight:bold"'; 507 | } else { 508 | $tableStyling = ' class="entry-view-field-name"'; 509 | } 510 | } 511 | 512 | $tableContents .= "\n".$entry_title."\n\n"; 513 | } else { 514 | $tableContents .= $entry_title.": "; 515 | } 516 | 517 | if (is_array($childValue)) { 518 | if (count($childValue) == 1) { 519 | $childValueOutput = apply_filters('gform_entry_field_value', reset($childValue), $form['fields'][$field_index], array(), $form); 520 | } elseif (count($childValue) > 1) { 521 | if ($form['fields'][$field_index]['type'] == 'date') { 522 | $childValueOutput = implode('/', $childValue); 523 | } else { 524 | if ($format == 'html') { 525 | if ($media == 'email') { 526 | $childValueOutput = "
      \n"; 527 | } else { 528 | $childValueOutput = "
        \n"; 529 | } 530 | } 531 | 532 | foreach ($childValue as $childValueData) { 533 | $childValueData = apply_filters('gform_entry_field_value', $childValueData, $form['fields'][$field_index], array(), $form); 534 | if ($format == 'html') { 535 | $childValueOutput .= "
      • ".$childValueData."
      • "; 536 | } else { 537 | $childValueOutput .= $childValueData."\n"; 538 | } 539 | } 540 | 541 | if ($format == 'html') { $childValueOutput .= "
      \n"; } 542 | } 543 | } 544 | 545 | if ($media == 'email') { $tableStyling = ''; } else { $tableStyling = ' class="entry-view-field-value"'; } 546 | 547 | if ($format == 'html') { 548 | $tableContents .= "\n".$childValueOutput."\n\n"; 549 | } else { 550 | $tableContents .= $childValueOutput."\n"; 551 | } 552 | } 553 | } 554 | 555 | if (!empty($tableContents)) { 556 | if ($format == 'html') { 557 | if ($media == 'email') { $tableStyling = ' width="100%" border="0" cellpadding="5" bgcolor="#FFFFFF"'; } else { $tableStyling = ' class="widefat fixed entry-detail-view"'; } 558 | $output .= "\n"; 559 | $output .= $tableContents; 560 | $output .= "
      \n"; 561 | } else { 562 | $output .= $tableContents."\n"; 563 | } 564 | } 565 | } 566 | } 567 | 568 | if ($count !== 0) { 569 | if ($format == 'text') { $output = rtrim($output); } 570 | if (GF_REPEATER_DEBUG && $format == 'html') { $output = '
      '.print_r($dataArray, true).'
      '.$output; } 571 | return $output; 572 | } else { return ''; } 573 | } 574 | 575 | public function get_value_merge_tag($value, $input_id, $entry, $form, $modifier, $raw_value, $url_encode, $esc_html, $format, $nl2br) { 576 | $output = GF_Field_Repeater::get_value_entry_detail($raw_value, '', false, $format, 'email'); 577 | $output = preg_replace("/[\r\n]+/", "\n", $output); 578 | return trim($output); 579 | } 580 | 581 | public function get_value_export($entry, $input_id = '', $use_text = false, $is_csv = false) { 582 | if (empty($input_id)) { $input_id = $this->id; } 583 | $output = rgar($entry, $input_id); 584 | $output = GF_Field_Repeater::get_value_entry_detail($output, '', false, 'text', 'email'); 585 | $output = preg_replace("/[\r\n]+/", ", ", trim($output)); 586 | return $output; 587 | } 588 | 589 | public static function gform_hide_children($form) { 590 | $form_id = $form['id']; 591 | $repeaterChildren = Array(); 592 | $grid_modified = false; 593 | $grid_meta = GFFormsModel::get_grid_column_meta($form_id); 594 | 595 | foreach($form['fields'] as $key=>$field) { 596 | if ($field->type == 'repeater') { 597 | if (is_array($field->repeaterChildren)) { $repeaterChildren = array_merge($repeaterChildren, $field->repeaterChildren); } 598 | } elseif ($field->type == 'repeater-end') { array_push($repeaterChildren, $field->id); } 599 | 600 | if (!empty($repeaterChildren)) { 601 | if (in_array($field->id, $repeaterChildren)) { 602 | unset($form['fields'][$key]); 603 | 604 | if (is_array($grid_meta)) { 605 | $grid_pos = array_search($field->id, $grid_meta); 606 | if ($grid_pos) { 607 | $grid_modified = true; 608 | unset($grid_meta[$grid_pos]); 609 | } 610 | } 611 | } 612 | } 613 | } 614 | 615 | if ($grid_modified) { GFFormsModel::update_grid_column_meta($form_id, $grid_meta); } 616 | 617 | $form['fields'] = array_values($form['fields']); 618 | 619 | return $form; 620 | } 621 | 622 | public static function gform_disable_ajax($args) { 623 | $get_form = GFFormsModel::get_form_meta_by_id($args['form_id']); 624 | $form = reset($get_form); 625 | 626 | if (GF_Field_Repeater::get_field_index($form) !== false) { 627 | $args['ajax'] = false; 628 | } 629 | 630 | return $args; 631 | } 632 | 633 | public static function gform_bypass_children_validation($form) { 634 | if (GF_Field_Repeater::get_field_index($form) === false) { return $form; } 635 | 636 | $repeaterChildren = Array(); 637 | 638 | foreach($form['fields'] as $key=>$field) { 639 | if ($field->type == 'repeater') { 640 | if (is_array($field->repeaterChildren)) { $repeaterChildren = array_merge($repeaterChildren, $field->repeaterChildren); } 641 | } 642 | 643 | if (!empty($repeaterChildren)) { 644 | if (in_array($field->id, $repeaterChildren) && !$field->adminOnly) { 645 | $form['fields'][$key]['adminOnly'] = true; 646 | $form['fields'][$key]['repeaterChildValidationHidden'] = true; 647 | } 648 | } 649 | } 650 | 651 | return $form; 652 | } 653 | 654 | public static function gform_unhide_children_validation($form) { 655 | if (GF_Field_Repeater::get_field_index($form) === false) { return $form; } 656 | 657 | foreach($form['fields'] as $key=>$field) { 658 | if ($field->repeaterChildValidationHidden) { 659 | $form['fields'][$key]['adminOnly'] = false; 660 | $form['fields'][$key]['repeaterChildValidationHidden'] = false; 661 | } 662 | } 663 | 664 | return $form; 665 | } 666 | 667 | public static function get_field_index($form, $key = 'type', $value = 'repeater') { 668 | if (is_array($form)) { 669 | if (!array_key_exists('fields', $form)) { return false; } 670 | } else { return false; } 671 | 672 | foreach ($form['fields'] as $field_key=>$field_value) { 673 | if (is_object($field_value)) { 674 | if (property_exists($field_value, $key)) { 675 | if ($field_value[$key] == $value) { return $field_key; } 676 | } 677 | } 678 | } 679 | 680 | return false; 681 | } 682 | 683 | public static function get_field_type($form, $id) { 684 | $field_index = GF_Field_Repeater::get_field_index($form, 'id', $id); 685 | if ($field_index !== false) { return $form['fields'][$field_index]['type']; } 686 | return false; 687 | } 688 | 689 | public static function get_field_id_number($field_id) { 690 | $field_id = explode('_', $field_id); 691 | return $field_id[2]; 692 | } 693 | 694 | public static function get_children_parems($form, $children_ids) { 695 | foreach($form['fields'] as $key=>$value) { 696 | if (in_array($value['id'], $children_ids)) { 697 | if ($value['inputName']) { 698 | $parems[$value['id']] = $value['inputName']; 699 | } elseif ($value['inputs']) { 700 | foreach($value['inputs'] as $key=>$value) { 701 | if ($value['name']) { $parems[$value['id']] = $value['name']; } 702 | } 703 | } 704 | } 705 | } 706 | if (!empty($parems)) { return $parems; } else { return false; } 707 | } 708 | 709 | public static function get_children_parem_values($form, $children_ids) { 710 | global $wp_filter; 711 | $children_parems = GF_Field_Repeater::get_children_parems($form, $children_ids); 712 | 713 | if (empty($children_parems)) { return false; } 714 | 715 | // Check the URL first 716 | foreach($_GET as $url_key=>$url_value) { 717 | $key = array_search($url_key, $children_parems); 718 | if ($key !== false) { 719 | $parems[$key][0] = $url_value; 720 | } else { 721 | $split_key = preg_split('/\D+\K/', $url_key); 722 | $key = array_search($split_key[0], $children_parems); 723 | if ($key !== false) { $parems[$key][$split_key[1]] = $url_value; } 724 | } 725 | } 726 | 727 | // Then check the filters 728 | foreach($wp_filter as $key=>$value) { 729 | $split_key = preg_split('/^gform_field_value_+\K/', $key); 730 | if (!empty($split_key[1])) { 731 | $key1 = array_search($split_key[1], $children_parems); 732 | if ($key1 !== false) { 733 | $parems[$key1][0] = apply_filters($key, ''); 734 | } else { 735 | $split_key2 = preg_split('/\D+\K/', $split_key[1]); 736 | $key2 = array_search($split_key2[0], $children_parems); 737 | if ($key2 !== false) { $parems[$key2][$split_key2[1]] = apply_filters($key, ''); } 738 | } 739 | } 740 | } 741 | if (!empty($parems)) { return $parems; } else { return false; } 742 | } 743 | 744 | public static function field_is_repeater_child($form, $field_id) { 745 | if (GF_Field_Repeater::get_field_index($form) === false) { return false; } 746 | 747 | $fieldIsRepeaterChild = false; 748 | $repeaterChildren = Array(); 749 | 750 | foreach($form['fields'] as $key=>$field) { 751 | if ($field->type == 'repeater') { 752 | if (is_array($field->repeaterChildren)) { $repeaterChildren = array_merge($repeaterChildren, $field->repeaterChildren); } 753 | } 754 | 755 | if (!empty($repeaterChildren)) { 756 | if ($field->id == $field_id && in_array($field->id, $repeaterChildren)) { 757 | $fieldIsRepeaterChild = true; 758 | break; 759 | } 760 | } 761 | } 762 | 763 | return $fieldIsRepeaterChild; 764 | } 765 | 766 | public static function set_counter_script($script, $form_id, $field_id, $max_length) { 767 | $get_form = GFFormsModel::get_form_meta_by_id($form_id); 768 | $form = reset($get_form); 769 | $field_id_num = GF_Field_Repeater::get_field_id_number($field_id); 770 | 771 | if (GF_Field_Repeater::field_is_repeater_child($form, $field_id_num)) { 772 | $script = ''; 773 | } 774 | 775 | return $script; 776 | } 777 | } 778 | GF_Fields::register(new GF_Field_Repeater()); 779 | -------------------------------------------------------------------------------- /js/gf-repeater.js: -------------------------------------------------------------------------------- 1 | var gfRepeater_debug = gfRepeater_php.debug; 2 | var gfRepeater_repeaters = {}; 3 | var gfRepeater_submitted = false; 4 | var gfRepeater_init_done = false; 5 | 6 | /* 7 | gfRepeater_getRepeaters() 8 | Collects all repeater info and stores it inside of the global array "gfRepeater_repeaters". - First phase of setup. 9 | */ 10 | function gfRepeater_getRepeaters() { 11 | var error = false; 12 | 13 | var repeaterData = jQuery('.gform_wrapper').has('.ginput_container_repeater').each(function(){ 14 | var repeaters = {}; 15 | var formId = this.id.split('_')[2]; 16 | var form = jQuery(this).children('form').first(); 17 | var repeaterId = 0; 18 | 19 | var repeaterFound = 0; 20 | var repeaterChildCount = 0; 21 | var repeaterParemCount = 0; 22 | var parentSection = null; 23 | var repeaterInfo = {}; 24 | var repeaterChildren = {}; 25 | var repeaterChildrenInputData = {}; 26 | var capturedData = {}; 27 | var dataElement; 28 | var startElement; 29 | var startIdNum; 30 | 31 | // Remove ajax action from form because ajax enabled forms are not yet supported. 32 | if (jQuery(form).attr('action') == '/ajax-test') { jQuery(form).removeAttr('action'); } 33 | 34 | // Check if our php filter failed to disable ajax, if so, cancel repeater js. 35 | if (jQuery(this).siblings('#gform_ajax_frame_'+formId).length) { error = true; return false; } 36 | 37 | jQuery(this).find('.gfield').each(function(){ 38 | if (repeaterFound == 0) { 39 | if (jQuery(this).has('.ginput_container_repeater').length) { 40 | // Repeater Start 41 | 42 | repeaterId += 1; 43 | 44 | startId = jQuery(this).attr('id'); 45 | startIdNum = Number(startId.split('_')[2]); 46 | 47 | if (gfRepeater_debug) { console.log('Form #'+formId+' - Repeater #'+repeaterId+' - Start: '+startId); } 48 | 49 | startElement = jQuery(this); 50 | dataElement = startElement.find('.gform_repeater'); 51 | 52 | repeaterInfo = jQuery(dataElement).val(); 53 | if (repeaterInfo) { repeaterInfo = JSON.parse(repeaterInfo); } 54 | 55 | if (jQuery.captures()) { 56 | capturedData = jQuery.captures(dataElement.attr('name')); 57 | if (capturedData) { 58 | capturedData = JSON.parse(capturedData); 59 | if (repeaterId == 1 && capturedData['formId'] == formId) { 60 | gfRepeater_submitted = true; 61 | } 62 | } 63 | } 64 | 65 | if (repeaterId == 1) { 66 | jQuery(form).capture(); 67 | } 68 | 69 | repeaterFound = 1; 70 | } 71 | } else { 72 | if (jQuery(this).has('.ginput_container_repeater').length) { error = true; return false; } 73 | if (jQuery(this).has('.ginput_container_repeater-end').length) { 74 | // Repeater End 75 | 76 | endId = jQuery(this).attr('id'); 77 | endIdNum = Number(endId.split('_')[2]); 78 | 79 | if (gfRepeater_debug) { 80 | console.log('Form #'+formId+' - Repeater #'+repeaterId+' - End: '+endId); 81 | console.log('Form #'+formId+' - Repeater #'+repeaterId+' - Children Found: '+(repeaterChildCount)); 82 | } 83 | 84 | var repeaterControllers = {}; 85 | var endElement = jQuery(this); 86 | var addElement = endElement.find('.gf_repeater_add'); 87 | var removeElement = endElement.find('.gf_repeater_remove'); 88 | var addFunction = 'gfRepeater_repeatRepeater('+formId+','+repeaterId+');'; 89 | var removeFunction = 'gfRepeater_unrepeatRepeater('+formId+','+repeaterId+');'; 90 | 91 | jQuery(addElement).attr({onclick:addFunction,onkeypress:addFunction}); 92 | jQuery(removeElement).attr({onclick:removeFunction,onkeypress:removeFunction}); 93 | 94 | repeaterControllers = { 95 | add:addElement, 96 | remove:removeElement, 97 | data:dataElement, 98 | start:startElement, 99 | end:endElement 100 | }; 101 | 102 | var repeaterSettings = {}; 103 | var repeaterStart = Number(repeaterInfo['start']); 104 | var repeaterMin = Number(repeaterInfo['min']); 105 | var repeaterMax = Number(repeaterInfo['max']); 106 | if (!repeaterStart || (repeaterMax && repeaterStart > repeaterMax)) { repeaterStart = 1; } 107 | if (!repeaterMin || (repeaterMax && repeaterMin > repeaterMax)) { repeaterMin = 1; } 108 | if (!repeaterMax || (repeaterMin && repeaterMax && repeaterMin > repeaterMax)) { repeaterMax = null; } 109 | 110 | var repeaterAnimations_default = { 111 | add: { 112 | class: '', 113 | css: {opacity: 0}, 114 | properties: {opacity: 1}, 115 | options: {duration: 300} 116 | }, 117 | remove: { 118 | class: '', 119 | css: {opacity: 1}, 120 | properties: {opacity: 0}, 121 | options: {duration: 200} 122 | } 123 | } 124 | 125 | if (repeaterInfo['animations']) { 126 | var repeaterAnimations = jQuery.merge(repeaterAnimations_default, repeaterInfo['animations']); 127 | } else { 128 | var repeaterAnimations = repeaterAnimations_default; 129 | } 130 | 131 | repeaterSettings = { 132 | start:repeaterStart, 133 | min:repeaterMin, 134 | max:repeaterMax, 135 | animations:repeaterAnimations 136 | }; 137 | 138 | var repeaterdata = {}; 139 | var repeaterTabIndex = Number(dataElement.attr('tabindex')); 140 | var prevRepeatCount = null; 141 | if (gfRepeater_submitted && capturedData) { prevRepeatCount = capturedData['repeatCount']; } 142 | 143 | repeaterdata = { 144 | repeatCount:1, 145 | prevRepeatCount:prevRepeatCount, 146 | childrenCount:repeaterChildCount, 147 | paremCount:repeaterParemCount, 148 | tabIndex:repeaterTabIndex, 149 | inputData:repeaterChildrenInputData, 150 | startIdNum:startIdNum, 151 | endIdNum:endIdNum 152 | }; 153 | 154 | repeaters[repeaterId] = { 155 | data:repeaterdata, 156 | settings:repeaterSettings, 157 | controllers:repeaterControllers, 158 | children:repeaterChildren 159 | }; 160 | 161 | // Set back to defaults for the next repeater 162 | repeaterFound = 0; 163 | repeaterChildCount = 0; 164 | repeaterParemCount = 0; 165 | parentSection = null; 166 | repeaterChildren = {}; 167 | repeaterChildrenInputData = {}; 168 | repeaterChildrenPrePopulate = {}; 169 | repeaterRequiredChildren = null; 170 | } else { 171 | // Repeater Child 172 | 173 | repeaterChildCount +=1; 174 | var childElement = jQuery(this); 175 | var childLabel = jQuery(this).children('.gfield_label').text(); 176 | var childId = jQuery(this).attr('id'); 177 | var childIdNum = childId.split('_')[2]; 178 | var childInputs = {}; 179 | var childInputNames = []; 180 | var childInputCount = 0; 181 | var childRequired = false; 182 | var childInfo = repeaterInfo['children'][childIdNum]; 183 | var childParentSection = parentSection; 184 | var childType; 185 | var inputMask; 186 | var conditionalLogic; 187 | var maxLength; 188 | var enableEnhancedUI; 189 | 190 | if (jQuery(this).has('.ginput_container').length) { 191 | var childContainerClasses = jQuery(this).find('.ginput_container').attr('class').split(/\s+/); 192 | var searchFor = 'ginput_container_'; 193 | 194 | jQuery.each(childContainerClasses, function(key, value){ 195 | if (value.slice(0, searchFor.length) == searchFor) { 196 | childType = value.slice(searchFor.length, value.length); 197 | } 198 | }); 199 | } else if (jQuery(this).hasClass('gform_hidden')) { 200 | childType = 'hidden'; 201 | } else if (jQuery(this).hasClass('gsection')) { 202 | childType = 'section'; 203 | } 204 | 205 | if (childType == 'section') { 206 | parentSection = repeaterChildCount; 207 | childParentSection = null; 208 | } 209 | 210 | if (childInfo['required']) { childRequired = true; } 211 | if (childInfo['inputMask']) { inputMask = childInfo['inputMask']; } 212 | if (childInfo['maxLength']) { maxLength = childInfo['maxLength']; } 213 | if (childInfo['enableEnhancedUI']) { enableEnhancedUI = childInfo['enableEnhancedUI']; } 214 | if (childInfo['conditionalLogic']) { 215 | conditionalLogic = childInfo['conditionalLogic']; 216 | conditionalLogic['skip'] = []; 217 | } 218 | 219 | if (gfRepeater_debug) { console.log('Form #'+formId+' - Repeater #'+repeaterId+' - Child #'+repeaterChildCount+' - Found: '+childId); } 220 | 221 | jQuery(this).find(':input').each(function(){ 222 | childInputCount += 1; 223 | var inputElement = jQuery(this); 224 | var inputId = jQuery(this).attr('id'); 225 | var inputName = jQuery(this).attr('name'); 226 | var inputName2; 227 | var inputDefaultValue = gfRepeater_getInputValue(inputElement); 228 | var inputPrePopulate = {}; 229 | 230 | if (inputName) { 231 | if (jQuery.inArray(inputName, childInputNames) == -1) { childInputNames.push(inputName); } 232 | if (inputName.slice(-2) == '[]') { inputName2 = inputName.slice(0, inputName.length - 2); } else { inputName2 = inputName; } 233 | 234 | if (childInfo['prePopulate']) { 235 | inputPrePopulate = childInfo['prePopulate']; 236 | 237 | if (inputPrePopulate) { 238 | jQuery.each(inputPrePopulate, function(key, value){ 239 | if (key > repeaterParemCount) { repeaterParemCount = Number(key); } 240 | }); 241 | } 242 | } 243 | }; 244 | 245 | childInputs[childInputCount] = { 246 | element:inputElement, 247 | id:inputId, 248 | name:inputName, 249 | defaultValue:inputDefaultValue, 250 | prePopulate:inputPrePopulate 251 | }; 252 | 253 | if (gfRepeater_debug) { console.log('Form #'+formId+' - Repeater #'+repeaterId+' - Child #'+repeaterChildCount+' - Input Found: '+inputId); } 254 | }); 255 | 256 | repeaterChildren[repeaterChildCount] = { 257 | element:childElement, 258 | id:childId, 259 | idNum:childIdNum, 260 | inputs:childInputs, 261 | inputCount:childInputCount, 262 | required:childRequired, 263 | type:childType, 264 | inputMask:inputMask, 265 | conditionalLogic:conditionalLogic, 266 | parentSection:childParentSection, 267 | maxLength:maxLength, 268 | enableEnhancedUI:enableEnhancedUI 269 | }; 270 | 271 | repeaterChildrenInputData[childIdNum] = childInputNames; 272 | } 273 | } 274 | }); 275 | 276 | if (gfRepeater_debug) { console.log('Form #'+formId+' - Repeaters Found: '+(repeaterId)); } 277 | if (repeaterFound !== 0) { error = true; return false; } 278 | 279 | if (!jQuery.isEmptyObject(repeaters)) { 280 | gfRepeater_repeaters[formId] = repeaters; 281 | return true; 282 | } else { error = true; return false; } 283 | }); 284 | 285 | if (!error) { return true; } else { return false; } 286 | } 287 | 288 | /* 289 | gfRepeater_setRepeaterChildAttrs(formId, repeaterId, repeaterChildElement, repeatId) 290 | Adds the repeater ID number and Count number to the end of repeater child ID and name. 291 | 292 | formId The form Id. 293 | repeaterId The repeater ID. 294 | repeaterChildElement The child element to run the function for. 295 | repeatId (Optional) The repeatId to assign the child to. If a number is not specified, one will be automatically assigned. A 1 is required the first time this function is used during the setup process. 296 | */ 297 | function gfRepeater_setRepeaterChildAttrs(formId, repeaterId, repeaterChildElement, repeatId) { 298 | var repeater = gfRepeater_repeaters[formId][repeaterId]; 299 | if (!repeatId) { var repeatId = repeater['data']['repeatCount'] + 1; } 300 | var childId = jQuery(repeaterChildElement).attr('id').split('-')[0]; 301 | var childKey = gfRepeater_getIndex(repeater['children'], 'id', childId); 302 | var checkValidation = jQuery('#gform_wrapper_' + formId).hasClass('gform_validation_error'); 303 | 304 | if (childKey) { 305 | var failedValidation = false; 306 | var child = repeater['children'][childKey]; 307 | var childRequired = child['required']; 308 | var childType = child['type']; 309 | var inputCount = child['inputCount']; 310 | var inputMask = child['inputMask']; 311 | var maxLength = child['maxLength']; 312 | var enableEnhancedUI = child['enableEnhancedUI']; 313 | var tabindex = repeater['data']['tabIndex']; 314 | 315 | var newRootId = childId+'-'+repeaterId+'-'+repeatId; 316 | jQuery(repeaterChildElement) 317 | .attr('id', newRootId) 318 | .attr('data-repeater-parentId', repeaterId) 319 | .attr('data-repeater-repeatId', repeatId) 320 | .attr('data-repeater-childId', childKey) 321 | .addClass('gf_repeater_child_field'); 322 | 323 | gfRepeater_replaceShortcodes(repeaterChildElement); 324 | gfRepeater_doShortcode(repeaterChildElement, 'count', repeatId); 325 | gfRepeater_doShortcode(repeaterChildElement, 'buttons', repeater['controllers']['add'].parent().clone()); 326 | gfRepeater_doShortcode(repeaterChildElement, 'add', repeater['controllers']['add'].clone()); 327 | gfRepeater_doShortcode(repeaterChildElement, 'remove', repeater['controllers']['remove'].clone()); 328 | 329 | var removeFunction = 'gfRepeater_unrepeatRepeater('+formId+','+repeaterId+','+repeatId+');'; 330 | jQuery(repeaterChildElement) 331 | .find('.gf_repeater_remove') 332 | .attr({onclick:removeFunction,onkeypress:removeFunction}) 333 | .show(); 334 | 335 | jQuery(repeaterChildElement) 336 | .find('.gf_repeater_add') 337 | .show(); 338 | 339 | jQuery(repeaterChildElement) 340 | .find('.ginput_counter') 341 | .remove(); 342 | 343 | jQuery.each(repeater['children'][childKey]['inputs'], function(key, value){ 344 | var inputId = this['id']; 345 | var inputName = this['name']; 346 | var prePopulate = ''; 347 | 348 | if (childType == 'radio' || childType == 'time' || childType == 'multiselect' || childType == 'date') { 349 | var inputElement = gfRepeater_findElementByNameOrId(repeaterChildElement, null, inputId); 350 | } else { 351 | var inputElement = gfRepeater_findElementByNameOrId(repeaterChildElement, inputName, inputId); 352 | } 353 | 354 | inputElement.attr('data-repeater-inputId', key); 355 | 356 | if (inputId) { 357 | var newInputId = inputId+'-'+repeaterId+'-'+repeatId; 358 | jQuery(inputElement).attr('id', newInputId); 359 | jQuery(repeaterChildElement).find("label[for^='"+inputId+"']").attr('for', newInputId); 360 | } 361 | 362 | if (inputName) { 363 | if (inputName.slice(-2) == '[]') { 364 | var newInputName = inputName.slice(0, inputName.length - 2)+'-'+repeaterId+'-'+repeatId+'[]'; 365 | } else { 366 | var newInputName = inputName+'-'+repeaterId+'-'+repeatId; 367 | } 368 | 369 | jQuery(inputElement) 370 | .attr('name', newInputName) 371 | 372 | if (tabindex) { 373 | jQuery(inputElement) 374 | .attr('tabindex', tabindex); 375 | } 376 | } 377 | 378 | if (inputMask) { jQuery(inputElement).mask(inputMask); } 379 | 380 | if (this['prePopulate'][repeatId]) { 381 | prePopulate = this['prePopulate'][repeatId]; 382 | } else if (this['prePopulate'][0]) { 383 | prePopulate = this['prePopulate'][0]; 384 | } 385 | 386 | if (prePopulate) { 387 | if (childType == 'checkbox' || childType == 'radio') { 388 | prePopulateValues = prePopulate.split(','); 389 | if (jQuery.inArray(key, prePopulateValues) !== -1) { 390 | prePopulate = true; 391 | } else { 392 | prePopulate = false; 393 | } 394 | } 395 | 396 | gfRepeater_setInputValue(inputElement, prePopulate); 397 | } 398 | 399 | if (window['gformInitDatepicker'] && childType == 'date' && inputCount == 2 && key == 1) { 400 | jQuery(inputElement) 401 | .removeClass('hasDatepicker') 402 | .datepicker('destroy') 403 | .siblings('.ui-datepicker-trigger').remove(); 404 | } 405 | 406 | if (maxLength) { 407 | jQuery(inputElement).textareaCount({ 408 | 'maxCharacterSize': maxLength, 409 | 'originalStyle': 'ginput_counter', 410 | 'displayFormat' : '#input of #max max characters' 411 | }); 412 | } 413 | 414 | if (enableEnhancedUI) { 415 | var initialWidth = jQuery(inputElement).css('width'); 416 | 417 | jQuery(inputElement) 418 | .css('width', initialWidth) 419 | .show() 420 | .siblings('.chosen-container').remove(); 421 | 422 | jQuery(inputElement) 423 | .chosen({width: initialWidth}); 424 | } 425 | 426 | if (gfRepeater_submitted) { 427 | if (newInputName) { 428 | var savedValue = jQuery.captures(newInputName); 429 | if (savedValue) { 430 | if ( jQuery.isArray(savedValue) ) { 431 | arrkey = parseInt(key, 10) - 1; 432 | gfRepeater_setInputValue(inputElement, savedValue[arrkey] ); 433 | } else { 434 | gfRepeater_setInputValue(inputElement, savedValue); 435 | } 436 | } 437 | } 438 | 439 | if (childRequired) { 440 | if (newInputName) { 441 | var splitName = newInputName.replace('.', '_').split(/(_|-)/); 442 | if (childType == 'name' && jQuery.inArray(splitName[4], ['3', '6']) == -1) { return true; } 443 | if (childType == 'address' && jQuery.inArray(splitName[4], ['2']) !== -1) { return true; } 444 | } 445 | 446 | var inputValue = gfRepeater_getInputValue(inputElement); 447 | if (!inputValue && repeatId <= repeater['data']['prevRepeatCount']) { 448 | failedValidation = true; 449 | } 450 | } 451 | } 452 | }); 453 | 454 | if (childRequired) { 455 | var childLabel = repeaterChildElement.children('.gfield_label'); 456 | 457 | repeaterChildElement.addClass('gfield_contains_required'); 458 | 459 | if (!childLabel.has('.gfield_required').length) { 460 | childLabel.append("*"); 461 | } 462 | 463 | if (gfRepeater_submitted && checkValidation) { 464 | if (failedValidation) { 465 | repeaterChildElement.addClass('gfield_error'); 466 | if (!repeaterChildElement.has('.validation_message').length) { 467 | repeaterChildElement.append("
      " + GF_Repeater_Phrases.field_required + "
      "); 468 | } 469 | } else { 470 | repeaterChildElement 471 | .removeClass('gfield_error') 472 | .find('.validation_message').remove(); 473 | } 474 | } 475 | } 476 | } 477 | } 478 | 479 | /* 480 | gfRepeater_resetRepeaterChildrenAttrs(formId, repeaterId) 481 | Resets all repeatId's so that they are chronological. 482 | */ 483 | function gfRepeater_resetRepeaterChildrenAttrs(formId, repeaterId) { 484 | var repeaterChildren = gfRepeater_select(formId, repeaterId); 485 | var x = 0; 486 | 487 | var repeater = gfRepeater_repeaters[formId][repeaterId]; 488 | 489 | jQuery(repeaterChildren).each(function(){ 490 | if (jQuery(this).attr('data-repeater-childid') == 1) { 491 | x += 1; 492 | } 493 | 494 | if (jQuery(this).attr('data-repeater-repeatid') !== x) { 495 | gfRepeater_setRepeaterChildAttrs(formId, repeaterId, jQuery(this), x); 496 | } 497 | }); 498 | } 499 | 500 | /* 501 | gfRepeater_conditionalLogic_set(formId, repeaterId, repeaterChildId, repeatId) 502 | Runs 'gfRepeater_conditionalLogic_do' and assigns change event for all fields involed with the repeaterChildElement's conditional logic. 503 | */ 504 | function gfRepeater_conditionalLogic_set(formId, repeaterId, repeaterChildId, repeatId) { 505 | gfRepeater_conditionalLogic_do(formId, repeaterId, repeaterChildId, repeatId); 506 | 507 | var repeater = gfRepeater_repeaters[formId][repeaterId]; 508 | var repeaterChild = repeater['children'][repeaterChildId] 509 | var conditionalLogic = repeaterChild['conditionalLogic']; 510 | 511 | jQuery.each(conditionalLogic['rules'], function(key, value){ 512 | var fieldId = value['fieldId']; 513 | var childId = gfRepeater_getIndex(repeater['children'], 'idNum', fieldId); 514 | 515 | if (childId !== false) { 516 | var inputs = gfRepeater_select(formId, repeaterId, repeatId, childId, '*'); 517 | } else { 518 | var inputs = jQuery('#field_' + formId + '_' + fieldId + ' :input'); 519 | repeatId = null; 520 | } 521 | 522 | jQuery.each(inputs, function(key, input){ 523 | jQuery(this).bind('propertychange change click keyup input paste', function(){ 524 | gfRepeater_conditionalLogic_do(formId, repeaterId, repeaterChildId, repeatId); 525 | }); 526 | }); 527 | }); 528 | } 529 | 530 | /* 531 | gfRepeater_conditionalLogic_setAll(formId, repeaterId, repeatId) 532 | Sets conditionalLogic for all children inside of a repeatId. 533 | */ 534 | function gfRepeater_conditionalLogic_setAll(formId, repeaterId, repeatId) { 535 | var repeater = gfRepeater_repeaters[formId][repeaterId]; 536 | jQuery.each(repeater['children'], function(key, value){ 537 | if (this.conditionalLogic) { 538 | gfRepeater_conditionalLogic_set(formId, repeaterId, key, repeatId); 539 | } 540 | }); 541 | } 542 | 543 | /* 544 | gfRepeater_conditionalLogic_do(formId, repeaterId, repeaterChildId, repeatId) 545 | Hides or Shows repeaterChildElement depending on conditional logic. 546 | */ 547 | function gfRepeater_conditionalLogic_do(formId, repeaterId, repeaterChildId, repeatId) { 548 | var repeater = gfRepeater_repeaters[formId][repeaterId]; 549 | var repeaterChild = repeater['children'][repeaterChildId] 550 | var conditionalLogic = repeaterChild['conditionalLogic']; 551 | var effectedIds = [repeaterChildId]; 552 | var conditions = []; 553 | var conditionsPassed = false; 554 | var hideField = false; 555 | 556 | jQuery.each(conditionalLogic['rules'], function(key, value){ 557 | var condition = false; 558 | var fieldId = value['fieldId']; 559 | var childId = gfRepeater_getIndex(repeater['children'], 'idNum', fieldId); 560 | 561 | if (childId !== false) { 562 | var child = repeater['children'][childId]; 563 | var childElement = gfRepeater_select(formId, repeaterId, repeatId, childId); 564 | 565 | if (child['type'] == 'checkbox' || child['type'] == 'radio') { 566 | var inputValue = gfRepeater_getChoiceValue(childElement); 567 | var multiInput = true; 568 | } else { 569 | var inputElement = gfRepeater_select(formId, repeaterId, repeatId, childId, 1); 570 | var inputValue = gfRepeater_getInputValue(inputElement); 571 | var multiInput = false; 572 | } 573 | } else { 574 | var fieldElement = jQuery('#field_' + formId + '_' + fieldId); 575 | var firstInput = fieldElement.find(':input').first(); 576 | 577 | if (firstInput.is(':checkbox, :radio')) { 578 | var inputValue = gfRepeater_getChoiceValue(fieldElement); 579 | var multiInput = true; 580 | } else { 581 | var inputValue = gfRepeater_getInputValue(firstInput); 582 | var multiInput = false; 583 | } 584 | } 585 | 586 | if (multiInput) { 587 | if (jQuery.inArray(value['value'], inputValue) !== -1) { inputValue = value['value']; } else { inputValue = false; } 588 | } 589 | 590 | condition = gf_matches_operation(inputValue, value['value'], value['operator']); 591 | conditions.push(condition); 592 | }); 593 | 594 | if (conditionalLogic['logicType'] == 'all') { 595 | if (jQuery.inArray(false, conditions) == -1) { conditionsPassed = true; } 596 | } else { 597 | if (jQuery.inArray(true, conditions) !== -1) { conditionsPassed = true; } 598 | } 599 | 600 | if ((conditionsPassed && conditionalLogic['actionType'] !== 'show') || (!conditionsPassed && conditionalLogic['actionType'] == 'show')) { 601 | hideField = true; 602 | } 603 | 604 | if (repeaterChild['type'] == 'section') { 605 | var sectionChildren = gfRepeater_getIndex(repeater['children'], 'parentSection', repeaterChildId, true); 606 | if (sectionChildren !== false) { effectedIds = effectedIds.concat(sectionChildren); } 607 | } 608 | 609 | jQuery.each(effectedIds, function(key, value){ 610 | var effectedChild = repeater['children'][value]; 611 | var effectedLogic = effectedChild['conditionalLogic']; 612 | var effectedElement = gfRepeater_select(formId, repeaterId, repeatId, value); 613 | var skipId = repeatId; 614 | 615 | if (skipId == null) { skipId = 'all'; } 616 | 617 | if (effectedElement.length) { 618 | if (hideField) { 619 | effectedElement.hide(); 620 | 621 | if (effectedLogic) { 622 | if (jQuery.inArray(skipId, effectedLogic['skip']) == -1) { 623 | effectedLogic['skip'].push(skipId); 624 | } 625 | } 626 | } else { 627 | effectedElement.show(); 628 | 629 | if (effectedLogic) { 630 | if (jQuery.inArray(skipId, effectedLogic['skip']) !== -1) { 631 | var skipIndex = effectedLogic['skip'].indexOf(skipId); 632 | effectedLogic['skip'].splice(skipIndex, 1); 633 | } 634 | } 635 | } 636 | } 637 | }); 638 | 639 | gfRepeater_updateDataElement(formId, repeaterId); 640 | } 641 | 642 | /* 643 | gfRepeater_doShortcode(element, shortcode, value) 644 | Finds the 'shortcode' inside of 'element' and replaces it's contents with 'value'. 645 | 646 | element The element to search inside. 647 | shortcode The shortcode to search for. 648 | value The value to put inside the shortcode. 649 | */ 650 | function gfRepeater_doShortcode(element, shortcode, value) { 651 | element.find('.gfRepeater-shortcode-'+shortcode).each(function(){ 652 | jQuery(this).html(value); 653 | }); 654 | } 655 | 656 | /* 657 | gfRepeater_replaceShortcodes(element) 658 | Replaces any repeater shortcodes with spans for those shortcodes. 659 | 660 | element The element to search and replace. 661 | */ 662 | function gfRepeater_replaceShortcodes(element) { 663 | var shortcodes = ['count', 'buttons', 'add', 'remove']; 664 | 665 | var elementChildren = element.find('*'); 666 | 667 | jQuery.each(shortcodes, function(key, shortcode) { 668 | elementChildren.each(function() { 669 | var childTextElements = jQuery(this).contents().filter(function () { 670 | return this.nodeType == 3; 671 | }); 672 | 673 | childTextElements.each(function() { 674 | var replaceWith = jQuery(this).text().replace('[gfRepeater-'+shortcode+']', ''); 675 | 676 | jQuery(this).replaceWith(replaceWith); 677 | }); 678 | }); 679 | 680 | var textElements = element.contents().filter(function () { 681 | return this.nodeType == 3; 682 | }); 683 | 684 | textElements.each(function() { 685 | var replaceWith = jQuery(this).text().replace('[gfRepeater-'+shortcode+']', ''); 686 | 687 | jQuery(this).replaceWith(replaceWith); 688 | }); 689 | 690 | // var html = element.html(); 691 | // element.html(html.replace('[gfRepeater-'+shortcode+']', '')); 692 | }); 693 | } 694 | 695 | /* 696 | gfRepeater_repeatRepeater(formId, repeaterId) 697 | Repeats the repeater once. 698 | 699 | formId The form Id. 700 | repeaterId The repeater ID number to repeat. 701 | */ 702 | function gfRepeater_repeatRepeater(formId, repeaterId) { 703 | gfRepeater_select(formId, repeaterId).stop(true, true); 704 | 705 | var repeater = gfRepeater_repeaters[formId][repeaterId]; 706 | var repeatId = repeater['data']['repeatCount'] + 1; 707 | if (repeater['settings']['max'] && repeater['data']['repeatCount'] >= repeater['settings']['max']) { return; } 708 | 709 | jQuery(repeater['controllers']['start']) 710 | .parents('form') 711 | .trigger('gform_repeater_before_repeat', [repeaterId, repeatId]); 712 | 713 | var animateAdd = repeater['settings']['animations']['add']; 714 | var lastElement = gfRepeater_select(formId, repeaterId).last(); 715 | var clonedElements = gfRepeater_select(formId, repeaterId, 1); 716 | 717 | animateAdd['options']['always'] = function(animation, jumpedToEnd) { 718 | jQuery(this).removeClass('gf_repeater_child_field_adding ' + animateAdd['class']); 719 | if (jQuery(this).attr('data-repeater-childid') == clonedElements.length) { gfRepeater_repeatRepeater_finish(); } 720 | }; 721 | 722 | jQuery.each(clonedElements, function(key, element){ 723 | var clonedElement = jQuery(element).clone(); 724 | var childId = clonedElement.attr('data-repeater-childid'); 725 | 726 | gfRepeater_resetInputs(formId, repeaterId, childId, clonedElement); 727 | gfRepeater_setRepeaterChildAttrs(formId, repeaterId, clonedElement); 728 | 729 | if (animateAdd['options']['duration'] > 0) { 730 | clonedElement 731 | .addClass('gf_repeater_child_field_adding ' + animateAdd['class']) 732 | .css(animateAdd['css']) 733 | .insertAfter(lastElement) 734 | .animate(animateAdd['properties'], animateAdd['options']) 735 | 736 | clonedElement 737 | .delay(animateAdd['options']['duration']) 738 | .dequeue(); 739 | } else { 740 | clonedElement.insertAfter(lastElement); 741 | if (childId == clonedElements.length) { gfRepeater_repeatRepeater_finish(); } 742 | } 743 | 744 | lastElement = clonedElement; 745 | }); 746 | 747 | function gfRepeater_repeatRepeater_finish() { 748 | gfRepeater_conditionalLogic_setAll(formId, repeaterId, repeatId); 749 | 750 | repeater['data']['repeatCount'] += 1; 751 | gfRepeater_updateDataElement(formId, repeaterId); 752 | gfRepeater_updateRepeaterControls(formId, repeaterId); 753 | 754 | if (window['gformInitDatepicker']) { gformInitDatepicker(); } 755 | 756 | jQuery(repeater['controllers']['start']) 757 | .parents('form') 758 | .trigger('gform_repeater_after_repeat', [repeaterId, repeatId]); 759 | 760 | if (gfRepeater_debug) { console.log('Form #'+formId+' - Repeater #'+repeaterId+' - repeated'); } 761 | } 762 | } 763 | 764 | /* 765 | gfRepeater_unrepeatRepeater(formId, repeaterId, repeatId) 766 | Un-repeats the repeater once. 767 | 768 | formId The form Id. 769 | repeaterId The repeater ID number to unrepeat. 770 | repeatId (Optional) The repeat ID number to unrepeat. If an ID number is not specified, the last one will be chosen. 771 | */ 772 | function gfRepeater_unrepeatRepeater(formId, repeaterId, repeatId) { 773 | gfRepeater_select(formId, repeaterId).stop(true, true); 774 | 775 | var repeater = gfRepeater_repeaters[formId][repeaterId]; 776 | if (repeater['data']['repeatCount'] <= repeater['settings']['min']) { return; } 777 | if (!repeatId) { var repeatId = repeater['data']['repeatCount']; } 778 | 779 | jQuery(repeater['controllers']['start']) 780 | .parents('form') 781 | .trigger('gform_repeater_before_unrepeat', [repeaterId, repeatId]); 782 | 783 | var animateRemove = repeater['settings']['animations']['remove']; 784 | var removedElements = gfRepeater_select(formId, repeaterId, repeatId); 785 | 786 | animateRemove['options']['always'] = function(animation, jumpedToEnd) { 787 | jQuery(this).remove(); 788 | if (jQuery(this).attr('data-repeater-childid') == removedElements.length) { gfRepeater_unrepeatRepeater_finish(); } 789 | }; 790 | 791 | jQuery.each(removedElements, function(index, element){ 792 | var removedElement = jQuery(element); 793 | var childId = removedElement.attr('data-repeater-childid'); 794 | 795 | if (animateRemove['options']['duration'] > 0) { 796 | removedElement 797 | .addClass('gf_repeater_child_field_removing ' + animateRemove['class']) 798 | .css(animateRemove['css']) 799 | .animate(animateRemove['properties'], animateRemove['options']) 800 | 801 | removedElement 802 | .delay(animateRemove['options']['duration']) 803 | .dequeue(); 804 | } else { 805 | removedElement.remove(); 806 | if (childId == removedElements.length) { gfRepeater_unrepeatRepeater_finish(); } 807 | } 808 | }); 809 | 810 | function gfRepeater_unrepeatRepeater_finish() { 811 | repeater['data']['repeatCount'] -= 1; 812 | gfRepeater_updateDataElement(formId, repeaterId); 813 | gfRepeater_updateRepeaterControls(formId, repeaterId); 814 | 815 | if (repeatId !== repeater['data']['repeatCount'] + 1) { 816 | gfRepeater_resetRepeaterChildrenAttrs(formId, repeaterId); 817 | } 818 | 819 | jQuery(repeater['controllers']['start']) 820 | .parents('form') 821 | .trigger('gform_repeater_after_unrepeat', [repeaterId, repeatId]); 822 | 823 | // Refresh all conditional logic for elements after the unrepeated one 824 | for(var i=(repeatId-1);i currentRepeatCount) { 877 | var timesY = timesX - currentRepeatCount; 878 | gfRepeater_repeatRepeaterTimes(formId, repeaterId, timesY); 879 | } else if (timesX < currentRepeatCount) { 880 | var timesY = currentRepeatCount - timesX; 881 | gfRepeater_unrepeatRepeaterTimes(formId, repeaterId, timesY); 882 | } 883 | } 884 | 885 | /* 886 | gfRepeater_updateRepeaterControls(formId, repeaterId) 887 | Updates the add and remove buttons for the repeater. If the minimum repeat number has been reached, the remove button is hidden. If the maximum number has been reached, the add button is hidden. 888 | 889 | formId The form Id. 890 | repeaterId The repeater ID number to update the controls for. 891 | */ 892 | function gfRepeater_updateRepeaterControls(formId, repeaterId) { 893 | var repeater = gfRepeater_repeaters[formId][repeaterId]; 894 | 895 | if (repeater['settings']['max']) { 896 | if (repeater['data']['repeatCount'] >= repeater['settings']['max']) { 897 | jQuery(repeater['controllers']['add']).hide(); 898 | } else { 899 | jQuery(repeater['controllers']['add']).show(); 900 | } 901 | } 902 | 903 | if (repeater['data']['repeatCount'] <= repeater['settings']['min']) { 904 | jQuery(repeater['controllers']['remove']).hide(); 905 | } else { 906 | jQuery(repeater['controllers']['remove']).show(); 907 | } 908 | } 909 | 910 | /* 911 | gfRepeater_resetInputs(formId, repeaterId, childId, repeaterChildElement) 912 | Resets all input elements inside of a repeater child. 913 | 914 | formId The form Id. 915 | repeaterId The repeater ID. 916 | childId The repeater child ID number. 917 | repeaterChildElement The repeater child element. 918 | */ 919 | function gfRepeater_resetInputs(formId, repeaterId, childId, repeaterChildElement) { 920 | var repeater = gfRepeater_repeaters[formId][repeaterId]; 921 | jQuery.each(repeater['children'][childId]['inputs'], function(key, value){ 922 | var inputId = this['id']; 923 | var inputName = this['name']; 924 | var inputDefaultValue = this['defaultValue']; 925 | var inputElement = gfRepeater_findElementByNameOrId(repeaterChildElement, inputName, inputId); 926 | 927 | if (inputElement) { 928 | gfRepeater_setInputValue(inputElement, inputDefaultValue); 929 | } 930 | }); 931 | } 932 | 933 | /* 934 | gfRepeater_select(formId, repeaterId, repeatId, childId, inputId) 935 | Selects an element depending on the variables passed. 936 | 937 | formId The form Id. 938 | repeaterId (Optional) The repeater Id. 939 | repeatId (Optional) The repeat Id. 940 | childId (Optional) The child Id. 941 | inputId (Optional) The input Id. Also accepts '*' to select all inputs. 942 | */ 943 | function gfRepeater_select(formId, repeaterId, repeatId, childId, inputId) { 944 | var selector = 'div#gform_wrapper_'+formId+'>form#gform_'+formId; 945 | if (repeaterId || repeatId || childId || inputId) { selector += '>.gform_body .gform_fields>.gfield.gf_repeater_child_field'; } 946 | if (repeaterId) { selector += '[data-repeater-parentid='+repeaterId+']'; } 947 | if (repeatId) { selector += '[data-repeater-repeatid='+repeatId+']'; } 948 | if (childId) { selector += '[data-repeater-childid='+childId+']'; } 949 | if (inputId) { 950 | if (inputId == '*') { 951 | selector += ' [data-repeater-inputid]'; 952 | } else { 953 | selector += ' [data-repeater-inputid='+inputId+']'; 954 | } 955 | } 956 | return jQuery(selector); 957 | } 958 | 959 | /* 960 | gfRepeater_findElementByNameOrId(searchElement, elementName, elementId) 961 | Searches for an an element inside of another element by ID or Name. If both an ID and a Name are supplied it will first try the Name and then the ID. 962 | 963 | searchElement Element to search inside. 964 | inputName (Optional) A element name to search for. 965 | inputId (Optional) A element ID to search for. 966 | */ 967 | function gfRepeater_findElementByNameOrId(searchElement, elementName, elementId) { 968 | if (elementName) { var foundElement = jQuery(searchElement).find("[name^='"+elementName+"']"); } 969 | if (!foundElement && elementId) { var foundElement = jQuery(searchElement).find("[id^='"+elementId+"']"); } 970 | if (foundElement) { return foundElement; } else { return false; } 971 | } 972 | 973 | /* 974 | gfRepeater_getIndex 975 | Searches 'object' where 'key' equals 'value'. 976 | Returns first result if multiple is false. 977 | Returns array with all key results if multiple is true. 978 | Returns false if nothing was found. 979 | 980 | object Object or array to search through. 981 | key Key to search for. 982 | value Value to search for. 983 | multiple Set to true to return all results in an array. 984 | */ 985 | function gfRepeater_getIndex(object, key, value, multiple) { 986 | var keys = []; 987 | 988 | jQuery.each(object, function(fieldKey, fieldValue){ 989 | if (fieldValue[key] == value) { 990 | keys.push(fieldKey); 991 | if (!multiple) { return false; } 992 | } 993 | }); 994 | 995 | if (keys.length) { 996 | if (multiple) { 997 | return keys; 998 | } else { return keys[0]; } 999 | } else { return false; } 1000 | } 1001 | 1002 | /* 1003 | gfRepeater_getChoiceValue(fieldElement) 1004 | Searches 'fieldElement' for checkboxes and radios. Returns an array with the values of all the values that are 'checked'. 1005 | 1006 | fieldElement The element to search in. 1007 | */ 1008 | function gfRepeater_getChoiceValue(fieldElement) { 1009 | var value = []; 1010 | jQuery(fieldElement).find(':checkbox, :radio').each(function(){ 1011 | if (jQuery(this).prop('checked') == true) { 1012 | var val = jQuery(this).val(); 1013 | value.push(val); 1014 | } 1015 | }); 1016 | return value; 1017 | } 1018 | 1019 | /* 1020 | gfRepeater_getInputValue(inputElement) 1021 | Gets the value of an input. 1022 | 1023 | inputElement The input element. 1024 | */ 1025 | function gfRepeater_getInputValue(inputElement) { 1026 | if (inputElement.is(':checkbox, :radio')) { 1027 | if (inputElement.prop('checked') == true) { return true; } else { return false; } 1028 | } else { 1029 | return inputElement.val(); 1030 | } 1031 | } 1032 | 1033 | /* 1034 | gfRepeater_setInputValue(inputElement, value) 1035 | Sets the value of an input. 1036 | 1037 | inputElement The input element. 1038 | inputValue The value to set to the input. 1039 | */ 1040 | function gfRepeater_setInputValue(inputElement, inputValue) { 1041 | if (inputElement.is(':checkbox, :radio')) { 1042 | if (inputValue) { inputElement.prop('checked', true) } else { inputElement.prop('checked', false) } 1043 | } else { 1044 | inputElement.val(inputValue); 1045 | } 1046 | } 1047 | 1048 | /* 1049 | gfRepeater_updateDataElement(formId, repeaterId) 1050 | Updates the data element for the repater. The data element stores information that is passed to PHP for processing. 1051 | 1052 | formId The form Id. 1053 | repeaterId The repeater ID number to update the data element for. 1054 | */ 1055 | function gfRepeater_updateDataElement(formId, repeaterId) { 1056 | var repeater = gfRepeater_repeaters[formId][repeaterId]; 1057 | var dataElement = jQuery(repeater['controllers']['data']); 1058 | 1059 | var dataArray = jQuery(dataElement).val(); 1060 | if (dataArray) { dataArray = JSON.parse(dataArray); } 1061 | 1062 | dataArray['repeaterId'] = repeaterId; 1063 | dataArray['repeatCount'] = repeater['data']['repeatCount']; 1064 | 1065 | jQuery.each(dataArray['children'], function(key, value){ 1066 | if (Array.isArray(this)) { dataArray['children'][key] = {}; } 1067 | var inputData = repeater['data']['inputData'][key]; 1068 | if (inputData.length) { 1069 | dataArray['children'][key]['inputs'] = inputData; 1070 | } 1071 | var fieldIndex = gfRepeater_getIndex(repeater['children'], 'idNum', key); 1072 | dataArray['children'][key]['conditionalLogic'] = repeater['children'][fieldIndex]['conditionalLogic']; 1073 | }); 1074 | 1075 | dataArray = JSON.stringify(dataArray); 1076 | jQuery(dataElement).val(dataArray); 1077 | } 1078 | 1079 | /* 1080 | gfRepeater_start() 1081 | Runs the gfRepeater_setRepeaterChildAttrs function for the first set of repeater children and then repeats the repeater a number of times depending on the repeater setting. - Second phase of setup. 1082 | */ 1083 | function gfRepeater_start() { 1084 | jQuery.each(gfRepeater_repeaters, function(key, repeater){ 1085 | var formId = key; 1086 | var form = gfRepeater_select(formId); 1087 | 1088 | jQuery.each(repeater, function(key, value){ 1089 | var repeaterId = key; 1090 | var repeater = gfRepeater_repeaters[formId][repeaterId]; 1091 | var repeatCount = repeater['settings']['start']; 1092 | var paremCount = repeater['data']['paremCount']; 1093 | var startIdNum = repeater['data']['startIdNum']; 1094 | 1095 | if (repeater['controllers']['data'].attr('data-required')) { repeater['controllers']['start'].addClass('gfield_contains_required'); } 1096 | 1097 | jQuery.each(repeater['children'], function(key, value){ 1098 | gfRepeater_setRepeaterChildAttrs(formId, repeaterId, jQuery(repeater['children'][key]['element']), 1); 1099 | if (this.conditionalLogic) { gfRepeater_conditionalLogic_set(formId, repeaterId, key, 1); } 1100 | }); 1101 | 1102 | if (gfRepeater_submitted) { 1103 | repeatCount = repeater['data']['prevRepeatCount']; 1104 | } else if (paremCount > repeatCount) { 1105 | repeatCount = paremCount; 1106 | } 1107 | 1108 | gfRepeater_setRepeater(formId, repeaterId, repeatCount); 1109 | gfRepeater_updateRepeaterControls(formId, repeaterId); 1110 | gfRepeater_updateDataElement(formId, repeaterId); 1111 | 1112 | jQuery(document).bind('gform_post_conditional_logic', function(e, formId, fields, isInit){ 1113 | if (fields) { 1114 | if (jQuery.inArray(startIdNum, fields) !== -1) { 1115 | var startElement = repeater['controllers']['start']; 1116 | var endElement = repeater['controllers']['end']; 1117 | var fieldDisplay = startElement.css('display'); 1118 | var repeaterChildren = gfRepeater_select(formId, repeaterId); 1119 | 1120 | repeaterChildren.css('display', fieldDisplay); 1121 | endElement.css('display', fieldDisplay); 1122 | } 1123 | } 1124 | }); 1125 | }); 1126 | 1127 | jQuery(form).trigger('gform_repeater_init_done'); 1128 | }); 1129 | 1130 | if (window['gformInitDatepicker']) { gformInitDatepicker(); } 1131 | } 1132 | 1133 | // Initiation after gravity forms has rendered. 1134 | jQuery(document).bind('gform_post_render', function(){ 1135 | if (gfRepeater_init_done == false && jQuery.isEmptyObject(gfRepeater_repeaters)) { 1136 | if (gfRepeater_getRepeaters()) { 1137 | gfRepeater_start(); 1138 | jQuery(window).trigger('gform_repeater_init_done'); 1139 | } else { 1140 | alert('Notice to website owner: There was an error with one of your repeaters. This is usually caused by having ajax enabled on the form, forgetting to include a repeater-end field, or by trying to nest repeaters.'); 1141 | } 1142 | 1143 | gfRepeater_init_done = true; 1144 | } 1145 | }); 1146 | 1147 | // Debug shortcuts 1148 | if (gfRepeater_debug) { 1149 | jQuery(window).keydown(function(event){ 1150 | // Up Arrow - Prints the contents of gfRepeater_repeaters into the console. 1151 | if (event.which == 38) { console.log(gfRepeater_repeaters); } 1152 | 1153 | // Down Arrow - Prints the captured form values into the console. 1154 | if (event.which == 40) { console.log(jQuery.captures()); } 1155 | }); 1156 | } --------------------------------------------------------------------------------