a4126766a0a8e7f4446f5d590467687e99def8b1
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / View / Form / Element / AbstractElementView.php
1 <?php
2 namespace TYPO3\CMS\Form\View\Form\Element;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18 use TYPO3\CMS\Form\Domain\Model\Element\AbstractElement;
19 use TYPO3\CMS\Form\Utility\FormUtility;
20
21 /**
22 * Abstract class for the form elements view
23 *
24 * @author Patrick Broens <patrick@patrickbroens.nl>
25 */
26 abstract class AbstractElementView {
27
28 /**
29 * The model for the current object
30 *
31 * @var AbstractElement
32 */
33 protected $model;
34
35 /**
36 * @var string
37 */
38 protected $expectedModelName;
39
40 /**
41 * Wrap for elements
42 *
43 * @var string
44 */
45 protected $elementWrap = '
46 <li>
47 <element />
48 </li>
49 ';
50
51 /**
52 * True if element needs no element wrap
53 * like <li>element</li>
54 *
55 * @var bool
56 */
57 protected $noWrap = FALSE;
58
59 /**
60 * Constructor
61 *
62 * @param AbstractElement $model Current elements model
63 */
64 public function __construct(AbstractElement $model) {
65 if ($this->isValidModel($model) === FALSE) {
66 throw new \RuntimeException('Unexpected model "' . get_class($model) . '".');
67 }
68 $this->model = $model;
69 }
70
71 /**
72 * Determines whether the model is expected in this object.
73 *
74 * @param AbstractElement $model
75 * @return bool
76 */
77 protected function isValidModel(AbstractElement $model) {
78 return is_a($model, $this->getExpectedModelName($model));
79 }
80
81 /**
82 * Gets the expected model name.
83 *
84 * @param AbstractElement $model
85 * @return string
86 */
87 protected function getExpectedModelName(AbstractElement $model) {
88 if (!isset($this->expectedModelName)) {
89 $specificName = FormUtility::getInstance()->getLastPartOfClassName($this);
90 $this->expectedModelName = 'TYPO3\\CMS\\Form\\Domain\\Model\\Element\\' . $specificName . 'Element';
91 }
92 return $this->expectedModelName;
93 }
94
95 /**
96 * Parse the XML of a view object,
97 * check the node type and name
98 * and add the proper XML part of child tags
99 * to the DOMDocument of the current tag
100 *
101 * @param \DOMDocument $dom
102 * @param \DOMNode $reference Current XML structure
103 * @return void
104 */
105 protected function parseXML(\DOMDocument $dom, \DOMNode $reference) {
106 $node = $reference->firstChild;
107 while (!is_null($node)) {
108 $deleteNode = FALSE;
109 $nodeType = $node->nodeType;
110 $nodeName = $node->nodeName;
111 switch ($nodeType) {
112 case XML_TEXT_NODE:
113 break;
114 case XML_ELEMENT_NODE:
115 switch ($nodeName) {
116 case 'containerWrap':
117 $this->replaceNodeWithFragment($dom, $node, $this->render('containerWrap'));
118 $deleteNode = TRUE;
119 break;
120 case 'elements':
121 $replaceNode = $this->getChildElements($dom);
122 $node->parentNode->replaceChild($replaceNode, $node);
123 break;
124 case 'button':
125
126 case 'fieldset':
127
128 case 'form':
129
130 case 'input':
131
132 case 'optgroup':
133
134 case 'select':
135 $this->setAttributes($node);
136 break;
137 case 'label':
138 if (!strrchr(get_class($this), 'AdditionalElement')) {
139 if ($this->model->additionalIsSet($nodeName)) {
140 $this->replaceNodeWithFragment($dom, $node, $this->getAdditional('label'));
141 }
142 $deleteNode = TRUE;
143 } else {
144 if ($this->model->additionalIsSet($nodeName)) {
145 $this->setAttributeWithValueofOtherAttribute($node, 'for', 'id');
146 } else {
147 $deleteNode = TRUE;
148 }
149 }
150 break;
151 case 'legend':
152 if (!strrchr(get_class($this), 'AdditionalElement')) {
153 if ($this->model->additionalIsSet($nodeName)) {
154 $this->replaceNodeWithFragment($dom, $node, $this->getAdditional('legend'));
155 }
156 $deleteNode = TRUE;
157 }
158 break;
159 case 'textarea':
160
161 case 'option':
162 $this->setAttributes($node);
163 $appendNode = $dom->createTextNode($this->getElementData());
164 $node->appendChild($appendNode);
165 break;
166 case 'errorvalue':
167
168 case 'labelvalue':
169
170 case 'legendvalue':
171
172 case 'mandatoryvalue':
173 $replaceNode = $dom->createTextNode($this->getAdditionalValue());
174 $node->parentNode->insertBefore($replaceNode, $node);
175 $deleteNode = TRUE;
176 break;
177 case 'mandatory':
178
179 case 'error':
180 if ($this->model->additionalIsSet($nodeName)) {
181 $this->replaceNodeWithFragment($dom, $node, $this->getAdditional($nodeName));
182 }
183 $deleteNode = TRUE;
184 break;
185 case 'content':
186
187 case 'header':
188
189 case 'textblock':
190 $replaceNode = $dom->createTextNode($this->getElementData(FALSE));
191 $node->parentNode->insertBefore($replaceNode, $node);
192 $deleteNode = TRUE;
193 break;
194 }
195 break;
196 }
197 // Parse the child nodes of this node if available
198 if ($node->hasChildNodes()) {
199 $this->parseXML($dom, $node);
200 }
201 // Get the current node for deletion if replaced. We need this because nextSibling can be empty
202 $oldNode = $node;
203 // Go to next sibling to parse
204 $node = $node->nextSibling;
205 // Delete the old node. This can only be done after going to the next sibling
206 if ($deleteNode) {
207 $oldNode->parentNode->removeChild($oldNode);
208 }
209 }
210 }
211
212 /**
213 * Get the content for the current object as DOMDocument
214 *
215 * @param string $type Type of element for layout
216 * @param bool $returnFirstChild If TRUE, the first child will be returned instead of the DOMDocument
217 * @return \DOMNode XML part of the view object
218 */
219 public function render($type = 'element', $returnFirstChild = TRUE) {
220 $useLayout = $this->getLayout((string)$type);
221 $dom = new \DOMDocument('1.0', 'utf-8');
222 $dom->formatOutput = TRUE;
223 $dom->preserveWhiteSpace = FALSE;
224 $dom->loadXML($useLayout);
225 $this->parseXML($dom, $dom);
226 if ($returnFirstChild) {
227 return $dom->firstChild;
228 } else {
229 return $dom;
230 }
231 }
232
233 /**
234 * Ask the layoutHandler to get the layout for this object
235 *
236 * @param string $type Layout type
237 * @return string HTML string of the layout to use for this element
238 */
239 public function getLayout($type) {
240 /** @var $layoutHandler \TYPO3\CMS\Form\Layout */
241 $layoutHandler = GeneralUtility::makeInstance(\TYPO3\CMS\Form\Layout::class);
242 switch ($type) {
243 case 'element':
244 $layoutDefault = $this->layout;
245 $objectClass = get_class($this);
246 $type = FormUtility::getInstance()->getLastPartOfClassName($this, TRUE);
247 if (strrchr($objectClass, 'AdditionalElement')) {
248 $additionalModel = $this->model->getAdditionalObjectByKey($type);
249 $layoutOverride = $additionalModel->getLayout();
250 } else {
251 $layoutOverride = $this->model->getLayout();
252 }
253 $layout = $layoutHandler->getLayoutByObject($type, $layoutDefault, $layoutOverride);
254 break;
255 case 'elementWrap':
256 $layoutDefault = $this->elementWrap;
257 $elementWrap = $layoutHandler->getLayoutByObject($type, $layoutDefault);
258 $layout = str_replace('<element />', $this->getLayout('element'), $elementWrap);
259 break;
260 case 'containerWrap':
261 $layoutDefault = $this->containerWrap;
262 $layout = $layoutHandler->getLayoutByObject($type, $layoutDefault);
263 break;
264 }
265 return $layout;
266 }
267
268 /**
269 * Replace the current node with a document fragment
270 *
271 * @param \DOMDocument $dom
272 * @param \DOMNode $node Current Node
273 * @param \DOMNode $value Value to import
274 * @return void
275 */
276 public function replaceNodeWithFragment(\DOMDocument $dom, \DOMNode $node, \DOMNode $value) {
277 $replaceNode = $dom->createDocumentFragment();
278 $domNode = $dom->importNode($value, TRUE);
279 $replaceNode->appendChild($domNode);
280 $node->parentNode->insertBefore($replaceNode, $node);
281 }
282
283 /**
284 * Set the attributes on the html tags according to the attributes that are
285 * assigned in the model for a certain element
286 *
287 * @param \DOMElement $domElement DOM element of the specific HTML tag
288 * @return void
289 */
290 public function setAttributes(\DOMElement $domElement) {
291 $attributes = $this->model->getAttributes();
292 foreach ($attributes as $key => $attribute) {
293 if (!empty($attribute)) {
294 $value = htmlspecialchars($attribute->getValue(), ENT_QUOTES);
295 if (!empty($value)) {
296 $domElement->setAttribute($key, $value);
297 }
298 }
299 }
300 }
301
302 /**
303 * Set a single attribute of a HTML tag specified by key
304 *
305 * @param \DOMElement $domElement DOM element of the specific HTML tag
306 * @param string $key Attribute key
307 * @return void
308 */
309 public function setAttribute(\DOMElement $domElement, $key) {
310 $value = htmlspecialchars($this->model->getAttributeValue((string)$key), ENT_QUOTES);
311 if (!empty($value)) {
312 $domElement->setAttribute($key, $value);
313 }
314 }
315
316 /**
317 * Sets the value of an attribute with the value of another attribute,
318 * for instance equalizing the name and id attributes for the form tag
319 *
320 * @param \DOMElement $domElement DOM element of the specific HTML tag
321 * @param string $key Key of the attribute which needs to be changed
322 * @param string $other Key of the attribute to take the value from
323 */
324 public function setAttributeWithValueofOtherAttribute(\DOMElement $domElement, $key, $other) {
325 $value = htmlspecialchars($this->model->getAttributeValue((string)$other), ENT_QUOTES);
326 if (!empty($value)) {
327 $domElement->setAttribute($key, $value);
328 }
329 }
330
331 /**
332 * Load and instantiate an additional object
333 *
334 * @param string $class Type of additional
335 * @return \TYPO3\CMS\Form\View\Form\Element\AbstractElementView
336 */
337 protected function createAdditional($class) {
338 $class = strtolower((string)$class);
339 $className = 'TYPO3\\CMS\\Form\\View\\Form\\Additional\\' . ucfirst($class) . 'AdditionalElementView';
340 return GeneralUtility::makeInstance($className, $this->model);
341 }
342
343 /**
344 * Create additional object by key and render the content
345 *
346 * @param string $key Type of additional
347 * @return \DOMNode
348 */
349 public function getAdditional($key) {
350 $additional = $this->createAdditional($key);
351 return $additional->render();
352 }
353
354 /**
355 * Get the content of tags
356 * like <option>content</option>
357 * or <textarea>content</textarea>
358 *
359 * @param bool $encodeSpecialCharacters Whether to encode the data
360 * @return string
361 */
362 public function getElementData($encodeSpecialCharacters = TRUE) {
363 $elementData = $this->model->getData();
364 if ($encodeSpecialCharacters) {
365 $elementData = htmlspecialchars($elementData, ENT_QUOTES);
366 }
367 return $elementData;
368 }
369
370 /**
371 * Return the id for the element wraps,
372 * like <li id="csc-form-"> ... </li>
373 *
374 * @return string
375 */
376 public function getElementWrapId() {
377 $elementId = (int)$this->model->getElementId();
378 $wrapId = 'csc-form-' . $elementId;
379 return $wrapId;
380 }
381
382 /**
383 * Returns the type for the element wraps,
384 * like <li class="csc-form-element csc-form-element-abstract">...</li>
385 *
386 * @return string
387 */
388 public function getElementWrapType() {
389 $elementType = strtolower(FormUtility::getInstance()->getLastPartOfClassName($this));
390 $wrapType = 'csc-form-element csc-form-element-' . $elementType;
391 return $wrapType;
392 }
393
394 /**
395 * Returns all element wraps.
396 *
397 * @return string
398 */
399 public function getElementWraps() {
400 $wraps = array(
401 $this->getElementWrapId(),
402 $this->getElementWrapType()
403 );
404 return implode(' ', $wraps);
405 }
406
407 /**
408 * Read the noWrap value of an element
409 * if TRUE the element does not need a element wrap
410 * like <li>element</li>
411 *
412 * @return bool
413 */
414 public function noWrap() {
415 return $this->noWrap;
416 }
417
418 }