1 <?php
2 /**
3 * @package Joomla.Platform
4 * @subpackage Form
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 jimport('joomla.filesystem.path');
13
14 /**
15 * The Field to load the form inside current form
16 *
17 * @Example with all attributes:
18 * <field name="field-name" type="subform"
19 * formsource="path/to/form.xml" min="1" max="3" multiple="true" buttons="add,remove,move"
20 * layout="joomla.form.field.subform.repeatable-table" groupByFieldset="false" component="com_example" client="site"
21 * label="Field Label" description="Field Description" />
22 *
23 * @since 3.6
24 */
25 class JFormFieldSubform extends JFormField
26 {
27 /**
28 * The form field type.
29 * @var string
30 */
31 protected $type = 'Subform';
32
33 /**
34 * Form source
35 * @var string
36 */
37 protected $formsource;
38
39 /**
40 * Minimum items in repeat mode
41 * @var int
42 */
43 protected $min = 0;
44
45 /**
46 * Maximum items in repeat mode
47 * @var int
48 */
49 protected $max = 1000;
50
51 /**
52 * Layout to render the form
53 * @var string
54 */
55 protected $layout = 'joomla.form.field.subform.default';
56
57 /**
58 * Whether group subform fields by it`s fieldset
59 * @var boolean
60 */
61 protected $groupByFieldset = false;
62
63 /**
64 * Which buttons to show in miltiple mode
65 * @var array $buttons
66 */
67 protected $buttons = array('add' => true, 'remove' => true, 'move' => true);
68
69 /**
70 * Method to get certain otherwise inaccessible properties from the form field object.
71 *
72 * @param string $name The property name for which to the the value.
73 *
74 * @return mixed The property value or null.
75 *
76 * @since 3.6
77 */
78 public function __get($name)
79 {
80 switch ($name)
81 {
82 case 'formsource':
83 case 'min':
84 case 'max':
85 case 'layout':
86 case 'groupByFieldset':
87 case 'buttons':
88 return $this->$name;
89 }
90
91 return parent::__get($name);
92 }
93
94 /**
95 * Method to set certain otherwise inaccessible properties of the form field object.
96 *
97 * @param string $name The property name for which to the the value.
98 * @param mixed $value The value of the property.
99 *
100 * @return void
101 *
102 * @since 3.6
103 */
104 public function __set($name, $value)
105 {
106 switch ($name)
107 {
108 case 'formsource':
109 $this->formsource = (string) $value;
110
111 // Add root path if we have a path to XML file
112 if (strrpos($this->formsource, '.xml') === strlen($this->formsource) - 4)
113 {
114 $this->formsource = JPath::clean(JPATH_ROOT . '/' . $this->formsource);
115 }
116
117 break;
118
119 case 'min':
120 $this->min = (int) $value;
121 break;
122
123 case 'max':
124 if ($value)
125 {
126 $this->max = max(1, (int) $value);
127 }
128 break;
129
130 case 'groupByFieldset':
131 if ($value !== null)
132 {
133 $value = (string) $value;
134 $this->groupByFieldset = !($value === 'false' || $value === 'off' || $value === '0');
135 }
136 break;
137
138 case 'layout':
139 $this->layout = (string) $value;
140
141 // Make sure the layout is not empty.
142 if (!$this->layout)
143 {
144 // Set default value depend from "multiple" mode
145 $this->layout = !$this->multiple ? 'joomla.form.field.subform.default' : 'joomla.form.field.subform.repeatable';
146 }
147
148 break;
149
150 case 'buttons':
151
152 if (!$this->multiple)
153 {
154 $this->buttons = array();
155 break;
156 }
157
158 if ($value && !is_array($value))
159 {
160 $value = explode(',', (string) $value);
161 $value = array_fill_keys(array_filter($value), true);
162 }
163
164 if ($value)
165 {
166 $value = array_merge(array('add' => false, 'remove' => false, 'move' => false), $value);
167 $this->buttons = $value;
168 }
169
170 break;
171
172 default:
173 parent::__set($name, $value);
174 }
175 }
176
177 /**
178 * Method to attach a JForm object to the field.
179 *
180 * @param SimpleXMLElement $element The SimpleXMLElement object representing the <field /> tag for the form field object.
181 * @param mixed $value The form field value to validate.
182 * @param string $group The field name group control value.
183 *
184 * @return boolean True on success.
185 *
186 * @since 3.6
187 */
188 public function setup(SimpleXMLElement $element, $value, $group = null)
189 {
190 if (!parent::setup($element, $value, $group))
191 {
192 return false;
193 }
194
195 foreach (array('formsource', 'min', 'max', 'layout', 'groupByFieldset', 'buttons') as $attributeName)
196 {
197 $this->__set($attributeName, $element[$attributeName]);
198 }
199
200 if ($this->value && is_string($this->value))
201 {
202 // Guess here is the JSON string from 'default' attribute
203 $this->value = json_decode($this->value, true);
204 }
205
206 if (!$this->formsource && $element->form)
207 {
208 // Set the formsource parameter from the content of the node
209 $this->formsource = $element->form->saveXML();
210 }
211
212 return true;
213 }
214
215 /**
216 * Method to get the field input markup.
217 *
218 * @return string The field input markup.
219 *
220 * @since 3.6
221 */
222 protected function getInput()
223 {
224 $value = $this->value ? $this->value : array();
225
226 // Prepare data for renderer
227 $data = parent::getLayoutData();
228 $tmpl = null;
229 $forms = array();
230 $control = $this->name;
231
232 try
233 {
234 // Prepare the form template
235 $formname = 'subform' . ($this->group ? $this->group . '.' : '.') . $this->fieldname;
236 $tmplcontrol = !$this->multiple ? $control : $control . '[' . $this->fieldname . 'X]';
237 $tmpl = JForm::getInstance($formname, $this->formsource, array('control' => $tmplcontrol));
238
239 // Prepare the forms for exiting values
240 if ($this->multiple)
241 {
242 $value = array_values($value);
243 $c = max($this->min, min(count($value), $this->max));
244 for ($i = 0; $i < $c; $i++)
245 {
246 $itemcontrol = $control . '[' . $this->fieldname . $i . ']';
247 $itemform = JForm::getInstance($formname . $i, $this->formsource, array('control' => $itemcontrol));
248
249 if (!empty($value[$i]))
250 {
251 $itemform->bind($value[$i]);
252 }
253
254 $forms[] = $itemform;
255 }
256 }
257 else
258 {
259 $tmpl->bind($value);
260 $forms[] = $tmpl;
261 }
262 }
263 catch (Exception $e)
264 {
265 return $e->getMessage();
266 }
267
268 $data['tmpl'] = $tmpl;
269 $data['forms'] = $forms;
270 $data['min'] = $this->min;
271 $data['max'] = $this->max;
272 $data['control'] = $control;
273 $data['buttons'] = $this->buttons;
274 $data['fieldname'] = $this->fieldname;
275 $data['groupByFieldset'] = $this->groupByFieldset;
276
277 // Prepare renderer
278 $renderer = $this->getRenderer($this->layout);
279
280 // Allow to define some JLayout options as attribute of the element
281 if ($this->element['component'])
282 {
283 $renderer->setComponent((string) $this->element['component']);
284 }
285
286 if ($this->element['client'])
287 {
288 $renderer->setClient((string) $this->element['client']);
289 }
290
291 // Render
292 $html = $renderer->render($data);
293
294 // Add hidden input on front of the subform inputs, in multiple mode
295 // for allow to submit an empty value
296 if ($this->multiple)
297 {
298 $html = '<input name="' . $this->name . '" type="hidden" value="" />' . $html;
299 }
300
301 return $html;
302 }
303
304 /**
305 * Method to get the name used for the field input tag.
306 *
307 * @param string $fieldName The field element name.
308 *
309 * @return string The name to be used for the field input tag.
310 *
311 * @since 3.6
312 */
313 protected function getName($fieldName)
314 {
315 $name = '';
316
317 // If there is a form control set for the attached form add it first.
318 if ($this->formControl)
319 {
320 $name .= $this->formControl;
321 }
322
323 // If the field is in a group add the group control to the field name.
324 if ($this->group)
325 {
326 // If we already have a name segment add the group control as another level.
327 $groups = explode('.', $this->group);
328
329 if ($name)
330 {
331 foreach ($groups as $group)
332 {
333 $name .= '[' . $group . ']';
334 }
335 }
336 else
337 {
338 $name .= array_shift($groups);
339
340 foreach ($groups as $group)
341 {
342 $name .= '[' . $group . ']';
343 }
344 }
345 }
346
347 // If we already have a name segment add the field name as another level.
348 if ($name)
349 {
350 $name .= '[' . $fieldName . ']';
351 }
352 else
353 {
354 $name .= $fieldName;
355 }
356
357 return $name;
358 }
359 }
360