[!!!][FEATURE] Streamline Fluid Styled Content and CSS Styled Content
[Packages/TYPO3.CMS.git] / typo3 / sysext / css_styled_content / Classes / Controller / CssStyledContentController.php
1 <?php
2 namespace TYPO3\CMS\CssStyledContent\Controller;
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\Core\Utility\MathUtility;
19
20 /**
21 * Plugin class - instantiated from TypoScript.
22 * Rendering some content elements from tt_content table.
23 */
24 class CssStyledContentController extends \TYPO3\CMS\Frontend\Plugin\AbstractPlugin
25 {
26 /**
27 * Same as class name
28 *
29 * @var string
30 */
31 public $prefixId = 'tx_cssstyledcontent_pi1';
32
33 /**
34 * Path to this script relative to the extension dir.
35 *
36 * @var string
37 */
38 public $scriptRelPath = 'Classes/Controller/CssStyledContentController.php';
39
40 /**
41 * The extension key
42 *
43 * @var string
44 */
45 public $extKey = 'css_styled_content';
46
47 /**
48 * @var array
49 */
50 public $conf = [];
51
52 /***********************************
53 * Rendering of Content Elements:
54 ***********************************/
55
56 /**
57 * Rendering the "Table" type content element, called from TypoScript (tt_content.table.20)
58 *
59 * @param string $content Content input. Not used, ignore.
60 * @param array $conf TypoScript configuration
61 * @return string HTML output.
62 */
63 public function render_table($content, $conf)
64 {
65 // Look for hook before running default code for function
66 if ($hookObj = $this->hookRequest('render_table')) {
67 return $hookObj->render_table($content, $conf);
68 } else {
69 // Init FlexForm configuration
70 $this->pi_initPIflexForm();
71 // Get bodytext field content
72 $field = isset($conf['field']) && trim($conf['field']) ? trim($conf['field']) : 'bodytext';
73 $content = trim($this->cObj->data[$field]);
74 if ($content === '') {
75 return '';
76 }
77 // Get configuration
78 $caption = trim($this->cObj->data['table_caption']);
79 $useTfoot = trim($this->cObj->data['table_tfoot']);
80 $headerPosition = trim($this->cObj->data['table_header_position']);
81 switch ($headerPosition) {
82 case '1':
83 $headerPos = 'top';
84 break;
85 case '2':
86 $headerPos = 'left';
87 break;
88 default:
89 $headerPos = '';
90 break;
91 }
92 $tableClass = trim($this->cObj->data['table_class']);
93 $delimiter = trim($this->cObj->data['table_delimiter']);
94 if ($delimiter) {
95 $delimiter = chr((int)$delimiter);
96 } else {
97 $delimiter = '|';
98 }
99 $quotedInput = trim($this->cObj->data['table_enclosure']);
100 if ($quotedInput) {
101 $quotedInput = chr((int)$quotedInput);
102 } else {
103 $quotedInput = '';
104 }
105 // Generate id prefix for accessible header
106 $headerScope = $headerPos === 'top' ? 'col' : 'row';
107 $headerIdPrefix = $headerScope . $this->cObj->data['uid'] . '-';
108 // Split into single lines (will become table-rows):
109 $rows = GeneralUtility::trimExplode(LF, $content);
110 reset($rows);
111 // Find number of columns to render:
112 $cols = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange(
113 $this->cObj->data['cols'] ? $this->cObj->data['cols'] : count(str_getcsv(current($rows), $delimiter, $quotedInput)),
114 0,
115 100
116 );
117 // Traverse rows (rendering the table here)
118 $rCount = count($rows);
119 foreach ($rows as $k => $v) {
120 $cells = str_getcsv($v, $delimiter, $quotedInput);
121 $newCells = [];
122 for ($a = 0; $a < $cols; $a++) {
123 if (trim($cells[$a]) === '') {
124 $cells[$a] = ' ';
125 }
126 $cells[$a] = preg_replace('|<br */?>|i', LF, $cells[$a]);
127 if ($headerPos === 'top' && !$k || $headerPos === 'left' && !$a) {
128 $scope = ' scope="' . $headerScope . '"';
129 $scope .= ' id="' . $headerIdPrefix . ($headerScope === 'col' ? $a : $k) . '"';
130 $newCells[$a] = '<th' . $scope . '>' . $this->cObj->stdWrap($cells[$a], $conf['innerStdWrap.']) . '</th>';
131 } else {
132 if (empty($headerPos)) {
133 $accessibleHeader = '';
134 } else {
135 $accessibleHeader = ' headers="' . $headerIdPrefix . ($headerScope === 'col' ? $a : $k) . '"';
136 }
137 $newCells[$a] = '<td' . $accessibleHeader . '>' . $this->cObj->stdWrap($cells[$a], $conf['innerStdWrap.']) . '</td>';
138 }
139 }
140 $rows[$k] = '<tr' . $rowAttribs . '>' . implode('', $newCells) . '</tr>';
141 }
142 $addTbody = 0;
143 $tableContents = '';
144 if ($caption) {
145 $tableContents .= '
146 <caption>' . $caption . '</caption>';
147 }
148 if ($headerPos === 'top' && $rows[0]) {
149 $tableContents .= '<thead>' . $rows[0] . '</thead>';
150 unset($rows[0]);
151 $addTbody = 1;
152 }
153 if ($useTfoot) {
154 $tableContents .= '<tfoot>' . $rows[$rCount - 1] . '</tfoot>';
155 unset($rows[$rCount - 1]);
156 $addTbody = 1;
157 }
158 $tmpTable = implode('', $rows);
159 if ($addTbody) {
160 $tmpTable = '<tbody>' . $tmpTable . '</tbody>';
161 }
162 $tableContents .= $tmpTable;
163 // Set header type:
164 $type = (int)$this->cObj->data['layout'];
165 // Table tag params.
166 $tableTagParams = [];
167 $tableTagParams['class'] = 'contenttable contenttable-' . $type . ($tableClass ? ' contenttable-' . $tableClass : '');
168 // Compile table output:
169 $out = '<table ' . GeneralUtility::implodeAttributes($tableTagParams) . '>' . $tableContents . '</table>';
170 // Return value
171 return $out;
172 }
173 }
174
175 /**
176 * Returns an array containing width relations for $colCount columns.
177 *
178 * Tries to use "colRelations" setting given by TS.
179 * uses "1:1" column relations by default.
180 *
181 * @param array $conf TS configuration for img
182 * @param int $colCount number of columns
183 * @return array
184 */
185 protected function getImgColumnRelations($conf, $colCount)
186 {
187 $relations = [];
188 $equalRelations = array_fill(0, $colCount, 1);
189 $colRelationsTypoScript = trim($this->cObj->stdWrap($conf['colRelations'], $conf['colRelations.']));
190 if ($colRelationsTypoScript) {
191 // Try to use column width relations given by TS
192 $relationParts = explode(':', $colRelationsTypoScript);
193 // Enough columns defined?
194 if (count($relationParts) >= $colCount) {
195 $out = [];
196 for ($a = 0; $a < $colCount; $a++) {
197 $currentRelationValue = (int)$relationParts[$a];
198 if ($currentRelationValue >= 1) {
199 $out[$a] = $currentRelationValue;
200 } else {
201 GeneralUtility::devLog('colRelations used with a value smaller than 1 therefore colRelations setting is ignored.', $this->extKey, 2);
202 unset($out);
203 break;
204 }
205 }
206 if (max($out) / min($out) <= 10) {
207 $relations = $out;
208 } else {
209 GeneralUtility::devLog(
210 'The difference in size between the largest and smallest colRelation was not within' .
211 ' a factor of ten therefore colRelations setting is ignored..',
212 $this->extKey,
213 2
214 );
215 }
216 }
217 }
218 return $relations ?: $equalRelations;
219 }
220
221 /**
222 * Returns an array containing the image widths for an image row with $colCount columns.
223 *
224 * @param array $conf TS configuration of img
225 * @param int $colCount number of columns
226 * @param int $netW max usable width for images (without spaces and borders)
227 * @return array
228 */
229 protected function getImgColumnWidths($conf, $colCount, $netW)
230 {
231 $columnWidths = [];
232 $colRelations = $this->getImgColumnRelations($conf, $colCount);
233 $accumWidth = 0;
234 $accumDesiredWidth = 0;
235 $relUnitCount = array_sum($colRelations);
236 for ($a = 0; $a < $colCount; $a++) {
237 // This much width is available for the remaining images in this row (int)
238 $availableWidth = $netW - $accumWidth;
239 // Theoretical width of resized image. (float)
240 $desiredWidth = $netW / $relUnitCount * $colRelations[$a];
241 // Add this width. $accumDesiredWidth becomes the desired horizontal position
242 $accumDesiredWidth += $desiredWidth;
243 // Calculate width by comparing actual and desired horizontal position.
244 // this evenly distributes rounding errors across all images in this row.
245 $suggestedWidth = round($accumDesiredWidth - $accumWidth);
246 // finalImgWidth may not exceed $availableWidth
247 $finalImgWidth = (int)min($availableWidth, $suggestedWidth);
248 $accumWidth += $finalImgWidth;
249 $columnWidths[$a] = $finalImgWidth;
250 }
251 return $columnWidths;
252 }
253
254 /**
255 * Rendering the text w/ image content element, called from TypoScript (tt_content.textpic.20)
256 *
257 * @param string $content Content input. Not used, ignore.
258 * @param array $conf TypoScript configuration. See TSRef "IMGTEXT". This function aims to be compatible.
259 * @return string HTML output.
260 */
261 public function render_textpic($content, $conf)
262 {
263 // Look for hook before running default code for function
264 if (method_exists($this, 'hookRequest') && ($hookObj = $this->hookRequest('render_textpic'))) {
265 return $hookObj->render_textpic($content, $conf);
266 }
267 $renderMethod = $this->cObj->stdWrap($conf['renderMethod'], $conf['renderMethod.']);
268 // Render using the default IMGTEXT code (table-based)
269 if (!$renderMethod || $renderMethod === 'table') {
270 return $this->cObj->cObjGetSingle('IMGTEXT', $conf);
271 }
272 $restoreRegisters = false;
273 if (isset($conf['preRenderRegisters.'])) {
274 $restoreRegisters = true;
275 $this->cObj->cObjGetSingle('LOAD_REGISTER', $conf['preRenderRegisters.']);
276 }
277 // Specific configuration for the chosen rendering method
278 if (is_array($conf['rendering.'][$renderMethod . '.'])) {
279 $conf = array_replace_recursive($conf, $conf['rendering.'][$renderMethod . '.']);
280 }
281 // Image or Text with Image?
282 if (is_array($conf['text.'])) {
283 $content = $this->cObj->stdWrap($this->cObj->cObjGet($conf['text.'], 'text.'), $conf['text.']);
284 }
285 $imgList = trim($this->cObj->stdWrap($conf['imgList'], $conf['imgList.']));
286 if (!$imgList) {
287 // No images, that's easy
288 if ($restoreRegisters) {
289 $this->cObj->cObjGetSingle('RESTORE_REGISTER', []);
290 }
291 return $content;
292 }
293 $imgs = GeneralUtility::trimExplode(',', $imgList, true);
294 if (empty($imgs)) {
295 // The imgList was not empty but did only contain empty values
296 if ($restoreRegisters) {
297 $this->cObj->cObjGetSingle('RESTORE_REGISTER', []);
298 }
299 return $content;
300 }
301 $imgStart = (int)$this->cObj->stdWrap($conf['imgStart'], $conf['imgStart.']);
302 $imgCount = count($imgs) - $imgStart;
303 $imgMax = (int)$this->cObj->stdWrap($conf['imgMax'], $conf['imgMax.']);
304 if ($imgMax) {
305 $imgCount = MathUtility::forceIntegerInRange($imgCount, 0, $imgMax);
306 }
307 $imgPath = $this->cObj->stdWrap($conf['imgPath'], $conf['imgPath.']);
308 // Does we need to render a "global caption" (below the whole image block)?
309 $renderGlobalCaption = !$conf['captionSplit'] && !$conf['imageTextSplit'] && is_array($conf['caption.']);
310 if ($imgCount == 1) {
311 // If we just have one image, the caption relates to the image, so it is not "global"
312 $renderGlobalCaption = false;
313 }
314 $imgListContainsReferenceUids = (bool)(isset($conf['imgListContainsReferenceUids.'])
315 ? $this->cObj->stdWrap($conf['imgListContainsReferenceUids'], $conf['imgListContainsReferenceUids.'])
316 : $conf['imgListContainsReferenceUids']);
317 // Use the calculated information (amount of images, if global caption is wanted) to choose a different rendering method for the images-block
318 $this->frontendController->register['imageCount'] = $imgCount;
319 $this->frontendController->register['renderGlobalCaption'] = $renderGlobalCaption;
320 $fallbackRenderMethod = '';
321 if ($conf['fallbackRendering']) {
322 $fallbackRenderMethod = $this->cObj->cObjGetSingle($conf['fallbackRendering'], $conf['fallbackRendering.']);
323 }
324 if ($fallbackRenderMethod && is_array($conf['rendering.'][$fallbackRenderMethod . '.'])) {
325 $conf = array_replace_recursive($conf, $conf['rendering.'][$fallbackRenderMethod . '.']);
326 }
327 // Set the accessibility mode which uses a different type of markup, used 4.7+
328 $accessibilityMode = false;
329 if (strpos(strtolower($renderMethod), 'caption') || strpos(strtolower($fallbackRenderMethod), 'caption')) {
330 $accessibilityMode = true;
331 }
332 // Global caption
333 $globalCaption = '';
334 if ($renderGlobalCaption) {
335 $globalCaption = $this->cObj->stdWrap($this->cObj->cObjGet($conf['caption.'], 'caption.'), $conf['caption.']);
336 }
337 // Positioning
338 $position = $this->cObj->stdWrap($conf['textPos'], $conf['textPos.']);
339 // 0,1,2 = center,right,left
340 $imagePosition = $position & 7;
341 // 0,8,16,24 (above,below,intext,intext-wrap)
342 $contentPosition = $position & 24;
343 $textMargin = (int)$this->cObj->stdWrap($conf['textMargin'], $conf['textMargin.']);
344 if (!$conf['textMargin_outOfText'] && $contentPosition < 16) {
345 $textMargin = 0;
346 }
347 $colspacing = (int)$this->cObj->stdWrap($conf['colSpace'], $conf['colSpace.']);
348 $border = (int)$this->cObj->stdWrap($conf['border'], $conf['border.']) ? 1 : 0;
349 $borderThickness = (int)$this->cObj->stdWrap($conf['borderThick'], $conf['borderThick.']);
350 $borderThickness = $borderThickness ?: 1;
351 $borderSpace = $conf['borderSpace'] && $border ? (int)$conf['borderSpace'] : 0;
352 // Generate cols
353 $cols = (int)$this->cObj->stdWrap($conf['cols'], $conf['cols.']);
354 $colCount = $cols > 1 ? $cols : 1;
355 if ($colCount > $imgCount) {
356 $colCount = $imgCount;
357 }
358 $rowCount = ceil($imgCount / $colCount);
359 // Generate rows
360 $rows = (int)$this->cObj->stdWrap($conf['rows'], $conf['rows.']);
361 if ($rows > 1) {
362 $rowCount = $rows;
363 if ($rowCount > $imgCount) {
364 $rowCount = $imgCount;
365 }
366 $colCount = $rowCount > 1 ? ceil($imgCount / $rowCount) : $imgCount;
367 }
368 // Max Width
369 $maxW = (int)$this->cObj->stdWrap($conf['maxW'], $conf['maxW.']);
370 $maxWInText = (int)$this->cObj->stdWrap($conf['maxWInText'], $conf['maxWInText.']);
371 $fiftyPercentWidthInText = round($maxW / 100 * 50);
372 // in Text
373 if ($contentPosition >= 16) {
374 if (!$maxWInText) {
375 // If maxWInText is not set, it's calculated to the 50% of the max
376 $maxW = $fiftyPercentWidthInText;
377 } else {
378 $maxW = $maxWInText;
379 }
380 }
381 // max usuable width for images (without spacers and borders)
382 $netW = $maxW - $colspacing * ($colCount - 1) - $colCount * $border * ($borderThickness + $borderSpace) * 2;
383 // Specify the maximum width for each column
384 $columnWidths = $this->getImgColumnWidths($conf, $colCount, $netW);
385 $image_frames = (int)$this->cObj->stdWrap($conf['image_frames.']['key'], $conf['image_frames.']['key.']);
386 // EqualHeight
387 $equalHeight = (int)$this->cObj->stdWrap($conf['equalH'], $conf['equalH.']);
388 if ($equalHeight) {
389 $relations_cols = [];
390 // contains the individual width of all images after scaling to $equalHeight
391 $imgWidths = [];
392 for ($a = 0; $a < $imgCount; $a++) {
393 $imgKey = $a + $imgStart;
394
395 /** @var $file \TYPO3\CMS\Core\Resource\File */
396 if (MathUtility::canBeInterpretedAsInteger($imgs[$imgKey])) {
397 if ($imgListContainsReferenceUids) {
398 $file = $this->getResourceFactory()->getFileReferenceObject((int)$imgs[$imgKey])->getOriginalFile();
399 } else {
400 $file = $this->getResourceFactory()->getFileObject((int)$imgs[$imgKey]);
401 }
402 } else {
403 $file = $this->getResourceFactory()->getFileObjectFromCombinedIdentifier($imgPath . $imgs[$imgKey]);
404 }
405
406 // relationship between the original height and the wished height
407 $rel = $file->getProperty('height') / $equalHeight;
408 // if relations is zero, then the addition of this value is omitted as the image is not expected to display because of some error.
409 if ($rel) {
410 $imgWidths[$a] = $file->getProperty('width') / $rel;
411 // counts the total width of the row with the new height taken into consideration.
412 $relations_cols[(int)floor($a / $colCount)] += $imgWidths[$a];
413 }
414 }
415 }
416 // Fetches pictures
417 $splitArr = [];
418 $splitArr['imgObjNum'] = $conf['imgObjNum'];
419 $splitArr = $this->frontendController->tmpl->splitConfArray($splitArr, $imgCount);
420 // Contains the width of every image row
421 $imageRowsFinalWidths = [];
422 // Array index of $imgsTag will be the same as in $imgs, but $imgsTag only contains the images that are actually shown
423 $imgsTag = [];
424 $origImages = [];
425 $rowIdx = 0;
426 for ($a = 0; $a < $imgCount; $a++) {
427 $imgKey = $a + $imgStart;
428 // If the image cannot be interpreted as integer (therefore filename and no FAL id), add the image path
429 if (MathUtility::canBeInterpretedAsInteger($imgs[$imgKey])) {
430 $totalImagePath = (int)$imgs[$imgKey];
431 $this->initializeCurrentFileInContentObjectRenderer($totalImagePath, $imgListContainsReferenceUids);
432 } else {
433 $totalImagePath = $imgPath . $imgs[$imgKey];
434 }
435 // register IMG_NUM is kept for backwards compatibility
436 $this->frontendController->register['IMAGE_NUM'] = $imgKey;
437 $this->frontendController->register['IMAGE_NUM_CURRENT'] = $imgKey;
438 $this->frontendController->register['ORIG_FILENAME'] = $totalImagePath;
439 $this->cObj->data[$this->cObj->currentValKey] = $totalImagePath;
440 $imgObjNum = (int)$splitArr[$a]['imgObjNum'];
441 $imgConf = $conf[$imgObjNum . '.'];
442 if ($equalHeight) {
443 if ($a % $colCount == 0) {
444 // A new row starts
445 // Reset accumulated net width
446 $accumWidth = 0;
447 // Reset accumulated desired width
448 $accumDesiredWidth = 0;
449 $rowTotalMaxW = $relations_cols[$rowIdx];
450 if ($rowTotalMaxW > $netW && $netW > 0) {
451 $scale = $rowTotalMaxW / $netW;
452 } else {
453 $scale = 1;
454 }
455 $desiredHeight = $equalHeight / $scale;
456 $rowIdx++;
457 }
458 // This much width is available for the remaining images in this row (int)
459 $availableWidth = $netW - $accumWidth;
460 // Theoretical width of resized image. (float)
461 $desiredWidth = $imgWidths[$a] / $scale;
462 // Add this width. $accumDesiredWidth becomes the desired horizontal position
463 $accumDesiredWidth += $desiredWidth;
464 // Calculate width by comparing actual and desired horizontal position.
465 // this evenly distributes rounding errors across all images in this row.
466 $suggestedWidth = round($accumDesiredWidth - $accumWidth);
467 // finalImgWidth may not exceed $availableWidth
468 $finalImgWidth = (int)min($availableWidth, $suggestedWidth);
469 $accumWidth += $finalImgWidth;
470 $imgConf['file.']['width'] = $finalImgWidth;
471 $imgConf['file.']['height'] = round($desiredHeight);
472 // other stuff will be calculated accordingly:
473 unset($imgConf['file.']['maxW']);
474 unset($imgConf['file.']['maxH']);
475 unset($imgConf['file.']['minW']);
476 unset($imgConf['file.']['minH']);
477 unset($imgConf['file.']['width.']);
478 unset($imgConf['file.']['maxW.']);
479 unset($imgConf['file.']['maxH.']);
480 unset($imgConf['file.']['minW.']);
481 unset($imgConf['file.']['minH.']);
482 } else {
483 $imgConf['file.']['maxW'] = $columnWidths[$a % $colCount];
484 }
485 $titleInLink = $this->cObj->stdWrap($imgConf['titleInLink'], $imgConf['titleInLink.']);
486 $titleInLinkAndImg = $this->cObj->stdWrap($imgConf['titleInLinkAndImg'], $imgConf['titleInLinkAndImg.']);
487 $oldATagParms = $this->frontendController->ATagParams;
488 if ($titleInLink) {
489 // Title in A-tag instead of IMG-tag
490 $titleText = trim($this->cObj->stdWrap($imgConf['titleText'], $imgConf['titleText.']));
491 if ($titleText) {
492 // This will be used by the IMAGE call later:
493 $this->frontendController->ATagParams .= ' title="' . htmlspecialchars($titleText) . '"';
494 }
495 }
496
497 // hook to allow custom rendering of a single element
498 // This hook is needed to render alternative content which is not just a plain image,
499 // like showing other FAL content, like videos, things which need to be embedded as JS, ...
500 $customRendering = '';
501 if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['css_styled_content']['pi1_hooks']['render_singleMediaElement'])
502 && is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['css_styled_content']['pi1_hooks']['render_singleMediaElement'])) {
503 $hookParameters = [
504 'file' => $totalImagePath,
505 'imageConfiguration' => $imgConf
506 ];
507
508 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['css_styled_content']['pi1_hooks']['render_singleMediaElement'] as $reference) {
509 $customRendering = \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction($reference, $hookParameters, $this);
510 // if there is a renderer found, don't run through the other renderers
511 if (!empty($customRendering)) {
512 break;
513 }
514 }
515 }
516
517 if (!empty($customRendering)) {
518 $imgsTag[$imgKey] = $customRendering;
519 } elseif ($imgConf || $imgConf['file']) {
520 if ($image_frames) {
521 if (is_array($conf['image_frames.'][$image_frames . '.'])) {
522 $imgConf['file.']['m.'] = $conf['image_frames.'][$image_frames . '.'];
523 }
524 }
525 if ($titleInLink && !$titleInLinkAndImg) {
526 // Check if the image will be linked
527 $link = $this->cObj->imageLinkWrap('', $this->cObj->getCurrentFile() ?: $totalImagePath, $imgConf['imageLinkWrap.']);
528 if ($link) {
529 // Title in A-tag only (set above: ATagParams), not in IMG-tag
530 unset($imgConf['titleText']);
531 unset($imgConf['titleText.']);
532 $imgConf['emptyTitleHandling'] = 'removeAttr';
533 }
534 }
535 $imgsTag[$imgKey] = $this->cObj->cObjGetSingle('IMAGE', $imgConf);
536 } else {
537 // currentValKey !!!
538 $imgsTag[$imgKey] = $this->cObj->cObjGetSingle('IMAGE', ['file' => $totalImagePath]);
539 }
540 // Restore our ATagParams
541 $this->frontendController->ATagParams = $oldATagParms;
542 // Store the original filepath
543 $origImages[$imgKey] = $this->frontendController->lastImageInfo;
544 if ($this->frontendController->lastImageInfo[0] == 0) {
545 $imageRowsFinalWidths[(int)floor($a / $colCount)] += $this->cObj->data['imagewidth'];
546 } else {
547 $imageRowsFinalWidths[(int)floor($a / $colCount)] += $this->frontendController->lastImageInfo[0];
548 }
549 }
550 // How much space will the image-block occupy?
551 $imageBlockWidth = max($imageRowsFinalWidths) + $colspacing * ($colCount - 1) + $colCount * $border * ($borderSpace + $borderThickness) * 2;
552 $this->frontendController->register['rowwidth'] = $imageBlockWidth;
553 $this->frontendController->register['rowWidthPlusTextMargin'] = $imageBlockWidth + $textMargin;
554 // Edit icons:
555 if (!is_array($conf['editIcons.'])) {
556 $conf['editIcons.'] = [];
557 }
558 $editIconsHTML = $conf['editIcons'] && $this->frontendController->beUserLogin ? $this->cObj->editIcons('', $conf['editIcons'], $conf['editIcons.']) : '';
559 $imageWrapCols = 1;
560 // User wants to separate the rows, but only do that if we do have rows
561 $separateRows = $this->cObj->stdWrap($conf['separateRows'], $conf['separateRows.']);
562 if ($rowCount == 1) {
563 $separateRows = 0;
564 }
565 if ($accessibilityMode) {
566 $imagesInColumns = round($imgCount / ($rowCount * $colCount), 0, PHP_ROUND_HALF_UP);
567 // Apply optionSplit to the list of classes that we want to add to each column
568 $addClassesCol = $conf['addClassesCol'];
569 if (isset($conf['addClassesCol.'])) {
570 $addClassesCol = $this->cObj->stdWrap($addClassesCol, $conf['addClassesCol.']);
571 }
572 $addClassesColConf = $this->frontendController->tmpl->splitConfArray(['addClassesCol' => $addClassesCol], $colCount);
573 // Apply optionSplit to the list of classes that we want to add to each image
574 $addClassesImage = $conf['addClassesImage'];
575 if (isset($conf['addClassesImage.'])) {
576 $addClassesImage = $this->cObj->stdWrap($addClassesImage, $conf['addClassesImage.']);
577 }
578 $addClassesImageConf = $this->frontendController->tmpl->splitConfArray(['addClassesImage' => $addClassesImage], $imagesInColumns);
579 $rows = [];
580 $currentImage = 0;
581 // Iterate over the rows
582 for ($rowCounter = 1; $rowCounter <= $rowCount; $rowCounter++) {
583 $rowColumns = [];
584 // Iterate over the columns
585 for ($columnCounter = 1; $columnCounter <= $colCount; $columnCounter++) {
586 $columnImages = [];
587 // Iterate over the amount of images allowed in a column
588 for ($imagesCounter = 1; $imagesCounter <= $imagesInColumns; $imagesCounter++) {
589 $image = null;
590 $splitCaption = null;
591 $imageMarkers = ($captionMarkers = []);
592 $single = '&nbsp;';
593 // Set the key of the current image
594 $imageKey = $currentImage + $imgStart;
595 // Register IMAGE_NUM_CURRENT for the caption
596 $this->frontendController->register['IMAGE_NUM_CURRENT'] = $imageKey;
597 $this->cObj->data[$this->cObj->currentValKey] = $origImages[$imageKey]['origFile'];
598 if (MathUtility::canBeInterpretedAsInteger($imgs[$imageKey])) {
599 $this->initializeCurrentFileInContentObjectRenderer((int)$imgs[$imageKey], $imgListContainsReferenceUids);
600 } elseif (!isset($imgs[$imageKey])) {
601 // If not all columns in the last row are filled $imageKey gets larger than
602 // the array. In that case we clear the current file.
603 $this->cObj->setCurrentFile(null);
604 }
605 // Get the image if not an empty cell
606 if (isset($imgsTag[$imageKey])) {
607 $image = $this->cObj->stdWrap($imgsTag[$imageKey], $conf['imgTagStdWrap.']);
608 // Add the edit icons
609 if ($editIconsHTML) {
610 $image .= $this->cObj->stdWrap($editIconsHTML, $conf['editIconsStdWrap.']);
611 }
612 // Wrap the single image
613 $single = $this->cObj->stdWrap($image, $conf['singleStdWrap.']);
614 // Get the caption
615 if (!$renderGlobalCaption) {
616 $imageMarkers['caption'] = $this->cObj->stdWrap($this->cObj->cObjGet($conf['caption.'], 'caption.'), $conf['caption.']);
617 $imageMarkers['caption'] = $this->cObj->substituteMarkerArray($imageMarkers['caption'], $captionMarkers, '###|###', 1, 1);
618 }
619 if ($addClassesImageConf[$imagesCounter - 1]['addClassesImage']) {
620 $imageMarkers['classes'] = ' ' . $addClassesImageConf[$imagesCounter - 1]['addClassesImage'];
621 }
622 }
623 $columnImages[] = $this->cObj->substituteMarkerArray($single, $imageMarkers, '###|###', 1, 1);
624 $currentImage++;
625 }
626 $rowColumn = $this->cObj->stdWrap(implode(LF, $columnImages), $conf['columnStdWrap.']);
627 // Start filling the markers for columnStdWrap
628 $columnMarkers = [];
629 if ($addClassesColConf[$columnCounter - 1]['addClassesCol']) {
630 $columnMarkers['classes'] = ' ' . $addClassesColConf[$columnCounter - 1]['addClassesCol'];
631 }
632 $rowColumns[] = $this->cObj->substituteMarkerArray($rowColumn, $columnMarkers, '###|###', 1, 1);
633 }
634 if ($rowCounter == $rowCount) {
635 $rowConfiguration = $conf['lastRowStdWrap.'];
636 } else {
637 $rowConfiguration = $conf['rowStdWrap.'];
638 }
639 $row = $this->cObj->stdWrap(implode(LF, $rowColumns), $rowConfiguration);
640 // Start filling the markers for columnStdWrap
641 $rowMarkers = [];
642 $rows[] = $this->cObj->substituteMarkerArray($row, $rowMarkers, '###|###', 1, 1);
643 }
644 $images = $this->cObj->stdWrap(implode(LF, $rows), $conf['allStdWrap.']);
645 // Start filling the markers for allStdWrap
646 $allMarkers = [];
647 $classes = [];
648 // Add the global caption to the allStdWrap marker array if set
649 if ($globalCaption) {
650 $allMarkers['caption'] = $globalCaption;
651 }
652 // Set the margin for image + text, no wrap always to avoid multiple stylesheets
653 $noWrapMargin = (int)(($maxWInText ? $maxWInText : $fiftyPercentWidthInText) + (int)$this->cObj->stdWrap($conf['textMargin'], $conf['textMargin.']));
654 $this->addPageStyle('.csc-textpic-intext-right-nowrap .csc-textpic-text', 'margin-right: ' . $noWrapMargin . 'px;');
655 $this->addPageStyle('.csc-textpic-intext-left-nowrap .csc-textpic-text', 'margin-left: ' . $noWrapMargin . 'px;');
656 // Beside Text where the image block width is not equal to maxW
657 if ($contentPosition == 24 && $maxW != $imageBlockWidth) {
658 $noWrapMargin = $imageBlockWidth + $textMargin;
659 // Beside Text, Right
660 if ($imagePosition == 1) {
661 $this->addPageStyle('.csc-textpic-intext-right-nowrap-' . $noWrapMargin . ' .csc-textpic-text', 'margin-right: ' . $noWrapMargin . 'px;');
662 $classes[] = 'csc-textpic-intext-right-nowrap-' . $noWrapMargin;
663 } elseif ($imagePosition == 2) {
664 $this->addPageStyle('.csc-textpic-intext-left-nowrap-' . $noWrapMargin . ' .csc-textpic-text', 'margin-left: ' . $noWrapMargin . 'px;');
665 $classes[] = 'csc-textpic-intext-left-nowrap-' . $noWrapMargin;
666 }
667 }
668 // Add the border class if needed
669 if ($border) {
670 $classes[] = $conf['borderClass'] ?: 'csc-textpic-border';
671 }
672 // Add the class for equal height if needed
673 if ($equalHeight) {
674 $classes[] = 'csc-textpic-equalheight';
675 }
676 $addClasses = $this->cObj->stdWrap($conf['addClasses'], $conf['addClasses.']);
677 if ($addClasses) {
678 $classes[] = $addClasses;
679 }
680 if ($classes) {
681 $class = ' ' . implode(' ', $classes);
682 }
683 // Fill the markers for the allStdWrap
684 $images = $this->cObj->substituteMarkerArray($images, $allMarkers, '###|###', 1, 1);
685 } else {
686 // Apply optionSplit to the list of classes that we want to add to each image
687 $addClassesImage = $conf['addClassesImage'];
688 if (isset($conf['addClassesImage.'])) {
689 $addClassesImage = $this->cObj->stdWrap($addClassesImage, $conf['addClassesImage.']);
690 }
691 $addClassesImageConf = $this->frontendController->tmpl->splitConfArray(['addClassesImage' => $addClassesImage], $colCount);
692 // Render the images
693 $images = '';
694 for ($c = 0; $c < $imageWrapCols; $c++) {
695 $tmpColspacing = $colspacing;
696 if ($c == $imageWrapCols - 1 && $imagePosition == 2 || $c == 0 && ($imagePosition == 1 || $imagePosition == 0)) {
697 // Do not add spacing after column if we are first column (left) or last column (center/right)
698 $tmpColspacing = 0;
699 }
700 $thisImages = '';
701 $allRows = '';
702 $maxImageSpace = 0;
703 $imgsTagCount = count($imgsTag);
704 for ($i = $c; $i < $imgsTagCount; $i = $i + $imageWrapCols) {
705 $imgKey = $i + $imgStart;
706 $colPos = $i % $colCount;
707 if ($separateRows && $colPos == 0) {
708 $thisRow = '';
709 }
710 // Render one image
711 if ($origImages[$imgKey][0] == 0) {
712 $imageSpace = $this->cObj->data['imagewidth'] + $border * ($borderSpace + $borderThickness) * 2;
713 } else {
714 $imageSpace = $origImages[$imgKey][0] + $border * ($borderSpace + $borderThickness) * 2;
715 }
716 $this->frontendController->register['IMAGE_NUM'] = $imgKey;
717 $this->frontendController->register['IMAGE_NUM_CURRENT'] = $imgKey;
718 $this->frontendController->register['ORIG_FILENAME'] = $origImages[$imgKey]['origFile'];
719 $this->frontendController->register['imagewidth'] = $origImages[$imgKey][0];
720 $this->frontendController->register['imagespace'] = $imageSpace;
721 $this->frontendController->register['imageheight'] = $origImages[$imgKey][1];
722 if (MathUtility::canBeInterpretedAsInteger($imgs[$imgKey])) {
723 $this->initializeCurrentFileInContentObjectRenderer(intval($imgs[$imgKey]), $imgListContainsReferenceUids);
724 }
725 if ($imageSpace > $maxImageSpace) {
726 $maxImageSpace = $imageSpace;
727 }
728 $thisImage = '';
729 $thisImage .= $this->cObj->stdWrap($imgsTag[$imgKey], $conf['imgTagStdWrap.']);
730 if (!$renderGlobalCaption) {
731 $thisImage .= $this->cObj->stdWrap($this->cObj->cObjGet($conf['caption.'], 'caption.'), $conf['caption.']);
732 }
733 if ($editIconsHTML) {
734 $thisImage .= $this->cObj->stdWrap($editIconsHTML, $conf['editIconsStdWrap.']);
735 }
736 $thisImage = $this->cObj->stdWrap($thisImage, $conf['oneImageStdWrap.']);
737 $classes = '';
738 if ($addClassesImageConf[$colPos]['addClassesImage']) {
739 $classes = ' ' . $addClassesImageConf[$colPos]['addClassesImage'];
740 }
741 $thisImage = str_replace('###CLASSES###', $classes, $thisImage);
742 if ($separateRows) {
743 $thisRow .= $thisImage;
744 } else {
745 $allRows .= $thisImage;
746 }
747 $this->frontendController->register['columnwidth'] = $maxImageSpace + $tmpColspacing;
748 // Close this row at the end (colCount), or the last row at the final end
749 if ($separateRows && $i + 1 === count($imgsTag)) {
750 // Close the very last row with either normal configuration or lastRow stdWrap
751 $allRows .= $this->cObj->stdWrap(
752 $thisRow,
753 is_array($conf['imageLastRowStdWrap.']) ? $conf['imageLastRowStdWrap.'] : $conf['imageRowStdWrap.']
754 );
755 } elseif ($separateRows && $colPos == $colCount - 1) {
756 $allRows .= $this->cObj->stdWrap($thisRow, $conf['imageRowStdWrap.']);
757 }
758 }
759 if ($separateRows) {
760 $thisImages .= $allRows;
761 }
762 $images .= $thisImages;
763 }
764 // Add the global caption, if not split
765 if ($globalCaption) {
766 $images .= $globalCaption;
767 }
768 // CSS-classes
769 $borderClass = '';
770 if ($border) {
771 $borderClass = $conf['borderClass'] ?: 'csc-textpic-border';
772 }
773 // Multiple classes with all properties, to be styled in CSS
774 $class = '';
775 $class .= $borderClass ? ' ' . $borderClass : '';
776 $class .= $equalHeight ? ' csc-textpic-equalheight' : '';
777 $addClasses = $this->cObj->stdWrap($conf['addClasses'], $conf['addClasses.']);
778 $class .= $addClasses ? ' ' . $addClasses : '';
779 // Do we need a width in our wrap around images?
780 $imgWrapWidth = '';
781 if ($position == 0 || $position == 8) {
782 // For 'center' we always need a width: without one, the margin:auto trick won't work
783 $imgWrapWidth = $imageBlockWidth;
784 }
785 if ($rowCount > 1) {
786 // For multiple rows we also need a width, so that the images will wrap
787 $imgWrapWidth = $imageBlockWidth;
788 }
789 if ($globalCaption) {
790 // If we have a global caption, we need the width so that the caption will wrap
791 $imgWrapWidth = $imageBlockWidth;
792 }
793 // Wrap around the whole image block
794 $this->frontendController->register['totalwidth'] = $imgWrapWidth;
795 if ($imgWrapWidth) {
796 $images = $this->cObj->stdWrap($images, $conf['imageStdWrap.']);
797 } else {
798 $images = $this->cObj->stdWrap($images, $conf['imageStdWrapNoWidth.']);
799 }
800 }
801
802 $output = str_replace(
803 [
804 '###TEXT###',
805 '###IMAGES###',
806 '###CLASSES###'
807 ],
808 [
809 $content,
810 $images,
811 $class
812 ],
813 $this->cObj->cObjGetSingle($conf['layout'], $conf['layout.'])
814 );
815
816 if ($restoreRegisters) {
817 $this->cObj->cObjGetSingle('RESTORE_REGISTER', []);
818 }
819
820 return $output;
821 }
822
823 /**
824 * Loads the file / file reference object and sets it in the
825 * currentFile property of the ContentObjectRenderer.
826 *
827 * This makes the file data available during image rendering.
828 *
829 * @param int $fileUid The UID of the file or file reference (depending on $treatAsReference) that should be loaded.
830 * @param bool $treatAsReference If TRUE the given UID will be used to load a file reference otherwise it will be used to load a regular file.
831 * @return void
832 */
833 protected function initializeCurrentFileInContentObjectRenderer($fileUid, $treatAsReference)
834 {
835 $resourceFactory = \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance();
836 if ($treatAsReference) {
837 $imageFile = $resourceFactory->getFileReferenceObject($fileUid);
838 } else {
839 $imageFile = $resourceFactory->getFileObject($fileUid);
840 }
841 $this->cObj->setCurrentFile($imageFile);
842 }
843
844 /***********************************
845 * Rendering of Content Element properties
846 ***********************************/
847
848 /**
849 * Add top or bottom margin to the content element
850 *
851 * Constructs and adds a class to the content element. This class selector
852 * and its declaration are added to the specific page styles.
853 *
854 * @param string $content Content input. Not used, ignore.
855 * @param array $configuration TypoScript configuration
856 * @return string The class name
857 */
858 public function renderSpace($content, array $configuration)
859 {
860 // Look for hook before running default code for function
861 if (method_exists($this, 'hookRequest') && ($hookObject = $this->hookRequest('renderSpace'))) {
862 return $hookObject->renderSpace($content, $configuration);
863 }
864 if (isset($configuration['space']) && in_array($configuration['space'], ['before', 'after'])) {
865 $constant = (int)$configuration['constant'];
866 if ($configuration['space'] === 'before') {
867 $value = $constant + $this->cObj->data['spaceBefore'];
868 $declaration = 'margin-top: ' . $value . 'px !important;';
869 } else {
870 $value = $constant + $this->cObj->data['spaceAfter'];
871 $declaration = 'margin-bottom: ' . $value . 'px !important;';
872 }
873 if (!empty($value)) {
874 if ($configuration['classStdWrap.']) {
875 $className = $this->cObj->stdWrap($value, $configuration['classStdWrap.']);
876 } else {
877 $className = $value;
878 }
879 $selector = '.' . trim($className);
880 $this->addPageStyle($selector, $declaration);
881 return $className;
882 }
883 }
884 }
885
886 /************************************
887 * Helper functions
888 ************************************/
889
890 /**
891 * Returns table attributes for tables. Not used anymore.
892 *
893 * @param array $conf TypoScript configuration array
894 * @param int $type The "layout" type
895 * @return array Array with attributes inside.
896 */
897 public function getTableAttributes($conf, $type)
898 {
899 // Initializing:
900 $tableTagParams_conf = $conf['tableParams_' . $type . '.'];
901 $border = $this->cObj->data['table_border'] ? (int)$this->cObj->data['table_border'] : $tableTagParams_conf['border'];
902 $cellSpacing = $this->cObj->data['table_cellspacing'] ? (int)$this->cObj->data['table_cellspacing'] : $tableTagParams_conf['cellspacing'];
903 $cellPadding = $this->cObj->data['table_cellpadding'] ? (int)$this->cObj->data['table_cellpadding'] : $tableTagParams_conf['cellpadding'];
904 $summary = trim(htmlspecialchars($this->pi_getFFvalue($this->cObj->data['pi_flexform'], 'acctables_summary')));
905 // Create table attributes and classes array:
906 $tableTagParams = ($classes = []);
907 // Table attributes for all doctypes except HTML5
908 if ($this->frontendController->config['config']['doctype'] !== 'html5') {
909 $tableTagParams['border'] = $border;
910 $tableTagParams['cellspacing'] = $cellSpacing;
911 $tableTagParams['cellpadding'] = $cellPadding;
912 if ($summary) {
913 $tableTagParams['summary'] = $summary;
914 }
915 } else {
916 if ($border) {
917 // Border property has changed, now with class
918 $borderClass = 'contenttable-border-' . $border;
919 $borderDeclaration = 'border-width: ' . $border . 'px; border-style: solid;';
920 $this->addPageStyle('.' . $borderClass, $borderDeclaration);
921 $classes[] = $borderClass;
922 }
923 if ($cellSpacing) {
924 // Border attribute for HTML5 is 1 when there is cell spacing
925 $tableTagParams['border'] = 1;
926 // Use CSS3 border-spacing in class to have cell spacing
927 $cellSpacingClass = 'contenttable-cellspacing-' . $cellSpacing;
928 $cellSpacingDeclaration = 'border-spacing: ' . $cellSpacing . 'px;';
929 $this->addPageStyle('.' . $cellSpacingClass, $cellSpacingDeclaration);
930 $classes[] = $cellSpacingClass;
931 }
932 if ($cellPadding) {
933 // Cell padding property has changed, now with class
934 $cellPaddingClass = 'contenttable-cellpadding-' . $cellPadding;
935 $cellSpacingSelector = '.' . $cellPaddingClass . ' td, .' . $cellPaddingClass . ' th';
936 $cellPaddingDeclaration = 'padding: ' . $cellPadding . 'px;';
937 $this->addPageStyle($cellSpacingSelector, $cellPaddingDeclaration);
938 $classes[] = $cellPaddingClass;
939 }
940 }
941 // Background color is class
942 if (isset($conf['color.'][$this->cObj->data['table_bgColor']]) && !empty($conf['color.'][$this->cObj->data['table_bgColor']])) {
943 $classes[] = 'contenttable-color-' . $this->cObj->data['table_bgColor'];
944 }
945 if (!empty($classes)) {
946 $tableTagParams['class'] = ' ' . implode(' ', $classes);
947 }
948 // Return result:
949 return $tableTagParams;
950 }
951
952 /**
953 * Add a style to the page, specific for this page
954 *
955 * The selector can be a contextual selector, like '#id .class p'
956 * The presence of the selector is checked to avoid multiple entries of the
957 * same selector.
958 *
959 * @param string $selector The selector
960 * @param string $declaration The declaration
961 * @return void
962 */
963 protected function addPageStyle($selector, $declaration)
964 {
965 if (!isset($this->frontendController->tmpl->setup['plugin.']['tx_cssstyledcontent.']['_CSS_PAGE_STYLE'])) {
966 $this->frontendController->tmpl->setup['plugin.']['tx_cssstyledcontent.']['_CSS_PAGE_STYLE'] = [];
967 }
968 if (!isset($this->frontendController->tmpl->setup['plugin.']['tx_cssstyledcontent.']['_CSS_PAGE_STYLE'][$selector])) {
969 $this->frontendController->tmpl->setup['plugin.']['tx_cssstyledcontent.']['_CSS_PAGE_STYLE'][$selector] = TAB . $selector . ' { ' . $declaration . ' }';
970 }
971 }
972
973 /**
974 * Returns an object reference to the hook object if any
975 *
976 * @param string $functionName Name of the function you want to call / hook key
977 * @return object|NULL Hook object, if any. Otherwise NULL.
978 */
979 public function hookRequest($functionName)
980 {
981 // Hook: menuConfig_preProcessModMenu
982 if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['css_styled_content']['pi1_hooks'][$functionName]) {
983 $hookObj = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['css_styled_content']['pi1_hooks'][$functionName]);
984 if (method_exists($hookObj, $functionName)) {
985 $hookObj->pObj = $this;
986 return $hookObj;
987 }
988 }
989 }
990
991 /**
992 * Get the ResourceFactory
993 *
994 * @return \TYPO3\CMS\Core\Resource\ResourceFactory
995 */
996 protected function getResourceFactory()
997 {
998 return \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance();
999 }
1000 }