From 13c67c454fb29462e7f5eb8bc86a30d9b84cfa45 Mon Sep 17 00:00:00 2001 From: Claus Due <claus@namelesscoder.net> Date: Wed, 5 Oct 2016 21:19:17 +0200 Subject: [PATCH] [FEATURE] Introduce PreviewRenderer pattern This introduces a new approach to registering and rendering previews; for content elements initially but possible to apply to any record type, and possible to call from other contexts than the PageLayoutView, e.g. AJAX-based preview rendering. Basically, this turns the old hook approach into a proper pattern where preview renderers are registered for a specific CType and must implement proper interfaces. A Resolver pattern is also introduced with a standard implementation and a standard renderer is registered for backwards compatibility. Resolves: #78450 Releases: master Change-Id: Ibf85d9b50b7bc6506d72c1ee63078373eaf9e433 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/50389 Tested-by: TYPO3com <noreply@typo3.com> Tested-by: Susanne Moog <look@susi.dev> Tested-by: Georg Ringer <georg.ringer@gmail.com> Reviewed-by: Susanne Moog <look@susi.dev> Reviewed-by: Georg Ringer <georg.ringer@gmail.com> --- .../Preview/PreviewRendererInterface.php | 82 ++++ .../PreviewRendererResolverInterface.php | 33 ++ .../StandardContentPreviewRenderer.php | 450 ++++++++++++++++++ .../StandardPreviewRendererResolver.php | 88 ++++ .../BackendLayout/Grid/GridColumnItem.php | 211 +------- .../Partials/PageLayout/Record/Footer.html | 12 +- typo3/sysext/backend/ext_localconf.php | 5 + ...ageLayoutViewClassInternalIsDeprecated.rst | 6 +- ...-78450-IntroducePreviewRendererPattern.rst | 158 ++++++ .../frontend/Configuration/TCA/tt_content.php | 1 + 10 files changed, 847 insertions(+), 199 deletions(-) create mode 100644 typo3/sysext/backend/Classes/Preview/PreviewRendererInterface.php create mode 100644 typo3/sysext/backend/Classes/Preview/PreviewRendererResolverInterface.php create mode 100644 typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php create mode 100644 typo3/sysext/backend/Classes/Preview/StandardPreviewRendererResolver.php create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-78450-IntroducePreviewRendererPattern.rst diff --git a/typo3/sysext/backend/Classes/Preview/PreviewRendererInterface.php b/typo3/sysext/backend/Classes/Preview/PreviewRendererInterface.php new file mode 100644 index 000000000000..0f902e852261 --- /dev/null +++ b/typo3/sysext/backend/Classes/Preview/PreviewRendererInterface.php @@ -0,0 +1,82 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Preview; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem; + +/** + * Interface PreviewRendererInterface + * + * Conctract for classes capable of rendering previews of a given record + * from a table. Responsible for rendering preview heeader, preview content + * and wrapping of those two values. + * + * Responsibilities are segmented into three methods, one for each responsibility, + * which is done in order to allow overriding classes to change those parts + * individually without having to replace other parts. Rather than relying on + * implementations to be friendly and divide code into smaller pieces and + * give them (at least) protected visibility, the key methods are instead required + * on the interface directly. + * + * Callers are then responsible for calling each method and combining/wrapping + * the output appropriately. + */ +interface PreviewRendererInterface +{ + /** + * Dedicated method for rendering preview header HTML for + * the page module only. Receives the the GridColumnItem + * that contains the record for which a preview header + * should be rendered and returned. + * + * @param GridColumnItem $item + * @return string + */ + public function renderPageModulePreviewHeader(GridColumnItem $item): string; + + /** + * Dedicated method for rendering preview body HTML for + * the page module only. Receives the the GridColumnItem + * that contains the record for which a preview should be + * rendered and returned. + * + * @param GridColumnItem $item + * @return string + */ + public function renderPageModulePreviewContent(GridColumnItem $item): string; + + /** + * Render a footer for the record to display in page module below + * the body of the item's preview. + * + * @param GridColumnItem $item + * @return string + */ + public function renderPageModulePreviewFooter(GridColumnItem $item): string; + + /** + * Dedicated method for wrapping a preview header and body + * HTML. Receives $item, an instance of GridColumnItem holding + * among other things the record, which can be used to determine + * appropriate wrapping. + * + * @param string $previewHeader + * @param string $previewContent + * @param GridColumnItem $item + * @return string + */ + public function wrapPageModulePreview(string $previewHeader, string $previewContent, GridColumnItem $item): string; +} diff --git a/typo3/sysext/backend/Classes/Preview/PreviewRendererResolverInterface.php b/typo3/sysext/backend/Classes/Preview/PreviewRendererResolverInterface.php new file mode 100644 index 000000000000..10832f8a88a0 --- /dev/null +++ b/typo3/sysext/backend/Classes/Preview/PreviewRendererResolverInterface.php @@ -0,0 +1,33 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Preview; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +/** + * Interface PreviewRendererResolverInterface + * + * Contract for classes capable of resolving PreviewRenderInterface + * implementations based on table and record. + */ +interface PreviewRendererResolverInterface +{ + /** + * @param string $table The name of the table the returned PreviewRenderer must work with + * @param array $row A record from $table which will be previewed - allows returning a different PreviewRenderer based on record attributes + * @param int $pageUid The UID of the page on which the preview will be rendered - allows returning a different PreviewRenderer based on for example pageTSconfig + * @return PreviewRendererInterface + */ + public function resolveRendererFor(string $table, array $row, int $pageUid): PreviewRendererInterface; +} diff --git a/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php b/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php new file mode 100644 index 000000000000..d64b26f9794a --- /dev/null +++ b/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php @@ -0,0 +1,450 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Preview; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use TYPO3\CMS\Backend\Routing\UriBuilder; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem; +use TYPO3\CMS\Backend\View\Drawing\DrawingConfiguration; +use TYPO3\CMS\Backend\View\PageLayoutView; +use TYPO3\CMS\Backend\View\PageLayoutViewDrawFooterHookInterface; +use TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Core\Imaging\Icon; +use TYPO3\CMS\Core\Imaging\IconFactory; +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Service\FlexFormService; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Fluid\View\StandaloneView; + +/** + * Class StandardContentPreviewRenderer + * + * Legacy preview rendering refactored from PageLayoutView. + * Provided as default preview rendering mechanism via + * StandardPreviewRendererResolver which detects the renderer + * based on TCA configuration. + * + * Can be replaced and/or subclassed by custom implementations + * by changing this TCA configuration. + * + * See also PreviewRendererInterface documentation. + */ +class StandardContentPreviewRenderer implements PreviewRendererInterface, LoggerAwareInterface +{ + use LoggerAwareTrait; + + public function renderPageModulePreviewHeader(GridColumnItem $item): string + { + $record = $item->getRecord(); + $itemLabels = $item->getBackendLayout()->getDrawingConfiguration()->getItemLabels(); + + $outHeader = ''; + + if ($record['header']) { + $infoArr = []; + $this->getProcessedValue($item, 'header_position,header_layout,header_link', $infoArr); + $hiddenHeaderNote = ''; + // If header layout is set to 'hidden', display an accordant note: + if ($record['header_layout'] == 100) { + $hiddenHeaderNote = ' <em>[' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.hidden')) . ']</em>'; + } + $outHeader = $record['date'] + ? htmlspecialchars($itemLabels['date'] . ' ' . BackendUtility::date($record['date'])) . '<br />' + : ''; + $outHeader .= '<strong>' . $this->linkEditContent($this->renderText($record['header']), $record) + . $hiddenHeaderNote . '</strong><br />'; + } + + return $outHeader; + } + + public function renderPageModulePreviewContent(GridColumnItem $item): string + { + $out = ''; + $record = $item->getRecord(); + + $contentTypeLabels = $item->getBackendLayout()->getDrawingConfiguration()->getContentTypeLabels(); + $languageService = $this->getLanguageService(); + + // Check if a Fluid-based preview template was defined for this CType + // and render it via Fluid. Possible option: + // mod.web_layout.tt_content.preview.media = EXT:site_mysite/Resources/Private/Templates/Preview/Media.html + $infoArr = []; + $this->getProcessedValue($item, 'header_position,header_layout,header_link', $infoArr); + $tsConfig = BackendUtility::getPagesTSconfig($record['pid'])['mod.']['web_layout.']['tt_content.']['preview.'] ?? []; + if (!empty($tsConfig[$record['CType']])) { + $fluidPreview = $this->renderContentElementPreviewFromFluidTemplate($record); + if ($fluidPreview !== null) { + return $fluidPreview; + } + } + + // Draw preview of the item depending on its CType + switch ($record['CType']) { + case 'header': + if ($record['subheader']) { + $out .= $this->linkEditContent($this->renderText($record['subheader']), $record) . '<br />'; + } + break; + case 'uploads': + if ($record['media']) { + $out .= $this->linkEditContent($this->getThumbCodeUnlinked($record, 'tt_content', 'media'), $record) . '<br />'; + } + break; + case 'menu': + $contentType = $contentTypeLabels[$record['CType']]; + $out .= $this->linkEditContent('<strong>' . htmlspecialchars($contentType) . '</strong>', $record) . '<br />'; + // Add Menu Type + $menuTypeLabel = $languageService->sL( + BackendUtility::getLabelFromItemListMerged($record['pid'], 'tt_content', 'menu_type', $record['menu_type']) + ); + $menuTypeLabel = $menuTypeLabel ?: 'invalid menu type'; + $out .= $this->linkEditContent($menuTypeLabel, $record); + if ($record['menu_type'] !== '2' && ($record['pages'] || $record['selected_categories'])) { + // Show pages if menu type is not "Sitemap" + $out .= ':' . $this->linkEditContent($this->generateListForCTypeMenu($record), $record) . '<br />'; + } + break; + case 'shortcut': + if (!empty($record['records'])) { + $shortcutContent = []; + $recordList = explode(',', $record['records']); + foreach ($recordList as $recordIdentifier) { + $split = BackendUtility::splitTable_Uid($recordIdentifier); + $tableName = empty($split[0]) ? 'tt_content' : $split[0]; + $shortcutRecord = BackendUtility::getRecord($tableName, $split[1]); + if (is_array($shortcutRecord)) { + $icon = $this->getIconFactory()->getIconForRecord($tableName, $shortcutRecord, Icon::SIZE_SMALL)->render(); + $icon = BackendUtility::wrapClickMenuOnIcon( + $icon, + $tableName, + $shortcutRecord['uid'], + 1, + '', + '+copy,info,edit,view' + ); + $shortcutContent[] = $icon + . htmlspecialchars(BackendUtility::getRecordTitle($tableName, $shortcutRecord)); + } + } + $out .= implode('<br />', $shortcutContent) . '<br />'; + } + break; + case 'list': + $hookOut = ''; + if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'])) { + $pageLayoutView = $this->createEmulatedPageLayoutViewFromDrawingConfiguration($item->getBackendLayout()->getDrawingConfiguration()); + $_params = ['pObj' => &$pageLayoutView, 'row' => $record]; + foreach ( + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'][$record['list_type']] ?? + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info']['_DEFAULT'] ?? + [] as $_funcRef + ) { + $hookOut .= GeneralUtility::callUserFunction($_funcRef, $_params, $pageLayoutView); + } + } + + if ((string)$hookOut !== '') { + $out .= $hookOut; + } elseif (!empty($record['list_type'])) { + $label = BackendUtility::getLabelFromItemListMerged($record['pid'], 'tt_content', 'list_type', $record['list_type']); + if (!empty($label)) { + $out .= $this->linkEditContent('<strong>' . htmlspecialchars($languageService->sL($label)) . '</strong>', $record) . '<br />'; + } else { + $message = sprintf($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.noMatchingValue'), $record['list_type']); + $out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>'; + } + } elseif (!empty($record['select_key'])) { + $out .= htmlspecialchars($languageService->sL(BackendUtility::getItemLabel('tt_content', 'select_key'))) + . ' ' . htmlspecialchars($record['select_key']) . '<br />'; + } else { + $out .= '<strong>' . $languageService->getLL('noPluginSelected') . '</strong>'; + } + $out .= htmlspecialchars($languageService->sL(BackendUtility::getLabelFromItemlist('tt_content', 'pages', $record['pages']))) . '<br />'; + break; + default: + $contentType = $contentTypeLabels[$record['CType']]; + + if (isset($contentType)) { + $out .= $this->linkEditContent('<strong>' . htmlspecialchars($contentType) . '</strong>', $record) . '<br />'; + if ($record['bodytext']) { + $out .= $this->linkEditContent($this->renderText($record['bodytext']), $record) . '<br />'; + } + if ($record['image']) { + $out .= $this->linkEditContent($this->getThumbCodeUnlinked($record, 'tt_content', 'image'), $record) . '<br />'; + } + } else { + $message = sprintf( + $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.noMatchingValue'), + $record['CType'] + ); + $out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>'; + } + } + + return $out; + } + + /** + * Render a footer for the record + * + * @param GridColumnItem $item + * @return string + */ + public function renderPageModulePreviewFooter(GridColumnItem $item): string + { + $content = ''; + $info = []; + $record = $item->getRecord(); + $this->getProcessedValue($item, 'starttime,endtime,fe_group,space_before_class,space_after_class', $info); + + if (!empty($GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']) && !empty($this->record[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']])) { + $info[] = $this->record[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']]; + } + + // Call drawFooter hooks + if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter'])) { + $pageLayoutView = $this->createEmulatedPageLayoutViewFromDrawingConfiguration($item->getBackendLayout()->getDrawingConfiguration()); + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter'] ?? [] as $className) { + $hookObject = GeneralUtility::makeInstance($className); + if (!$hookObject instanceof PageLayoutViewDrawFooterHookInterface) { + throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawFooterHookInterface::class, 1582574541); + } + $hookObject->preProcess($pageLayoutView, $info, $record); + } + } + + if (!empty($info)) { + $content = implode('<br>', $info); + } + + if (!empty($content)) { + $content = '<div class="t3-page-ce-footer">' . $content . '</div>'; + } + + return $content; + } + + public function wrapPageModulePreview(string $previewHeader, string $previewContent, GridColumnItem $item): string + { + $drawItem = true; + $record = $item->getRecord(); + $hookPreviewContent = ''; + + // Hook: Render an own preview of a record + if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'])) { + $pageLayoutView = $this->createEmulatedPageLayoutViewFromDrawingConfiguration($item->getBackendLayout()->getDrawingConfiguration()); + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'] ?? [] as $className) { + $hookObject = GeneralUtility::makeInstance($className); + if (!$hookObject instanceof PageLayoutViewDrawItemHookInterface) { + throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawItemHookInterface::class, 1582574553); + } + $hookObject->preProcess($pageLayoutView, $drawItem, $previewHeader, $hookPreviewContent, $record); + } + } + + $content = $previewHeader; + + $content .= $hookPreviewContent; + if ($drawItem) { + $content .= $previewContent; + } + + $out = '<span class="exampleContent">' . $content . '</span>'; + + if ($item->isDisabled()) { + return '<span class="text-muted">' . $out . '</span>'; + } + return $out; + } + + protected function getProcessedValue(GridColumnItem $item, string $fieldList, array &$info): void + { + $itemLabels = $item->getBackendLayout()->getDrawingConfiguration()->getItemLabels(); + $record = $item->getRecord(); + $fieldArr = explode(',', $fieldList); + foreach ($fieldArr as $field) { + if ($record[$field]) { + $info[] = '<strong>' . htmlspecialchars($itemLabels[$field]) . '</strong> ' + . htmlspecialchars(BackendUtility::getProcessedValue('tt_content', $field, $record[$field])); + } + } + } + + protected function renderContentElementPreviewFromFluidTemplate(array $row): ?string + { + $tsConfig = BackendUtility::getPagesTSconfig($row['pid'])['mod.']['web_layout.']['tt_content.']['preview.'] ?? []; + $fluidTemplateFile = ''; + + if ($row['CType'] === 'list' && !empty($row['list_type']) + && !empty($tsConfig['list.'][$row['list_type']]) + ) { + $fluidTemplateFile = $tsConfig['list.'][$row['list_type']]; + } elseif (!empty($tsConfig[$row['CType']])) { + $fluidTemplateFile = $tsConfig[$row['CType']]; + } + + if ($fluidTemplateFile) { + $fluidTemplateFile = GeneralUtility::getFileAbsFileName($fluidTemplateFile); + if ($fluidTemplateFile) { + try { + $view = GeneralUtility::makeInstance(StandaloneView::class); + $view->setTemplatePathAndFilename($fluidTemplateFile); + $view->assignMultiple($row); + if (!empty($row['pi_flexform'])) { + $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); + $view->assign('pi_flexform_transformed', $flexFormService->convertFlexFormContentToArray($row['pi_flexform'])); + } + return $view->render(); + } catch (\Exception $e) { + $this->logger->warning(sprintf( + 'The backend preview for content element %d can not be rendered using the Fluid template file "%s": %s', + $row['uid'], + $fluidTemplateFile, + $e->getMessage() + )); + + if ($GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] && $this->getBackendUser()->isAdmin()) { + $view = GeneralUtility::makeInstance(StandaloneView::class); + $view->assign('error', [ + 'message' => str_replace(Environment::getProjectPath(), '', $e->getMessage()), + 'title' => 'Error while rendering FluidTemplate preview using ' . str_replace(Environment::getProjectPath(), '', $fluidTemplateFile), + ]); + $view->setTemplateSource('<f:be.infobox title="{error.title}" state="2">{error.message}</f:be.infobox>'); + return $view->render(); + } + } + } + } + return null; + } + + /** + * Create thumbnail code for record/field but not linked + * + * @param mixed[] $row Record array + * @param string $table Table (record is from) + * @param string $field Field name for which thumbnail are to be rendered. + * @return string HTML for thumbnails, if any. + */ + protected function getThumbCodeUnlinked($row, $table, $field): string + { + return BackendUtility::thumbCode($row, $table, $field, '', '', null, 0, '', '', false); + } + + /** + * Processing of larger amounts of text (usually from RTE/bodytext fields) with word wrapping etc. + * + * @param string $input Input string + * @return string Output string + */ + protected function renderText(string $input): string + { + $input = strip_tags($input); + $input = GeneralUtility::fixed_lgd_cs($input, 1500); + return nl2br(htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8', false)); + } + + /** + * Generates a list of selected pages or categories for the CType menu + * + * @param array $record row from pages + * @return string + */ + protected function generateListForCTypeMenu(array $record): string + { + $table = 'pages'; + $field = 'pages'; + // get categories instead of pages + if (strpos($record['menu_type'], 'categorized_') !== false) { + $table = 'sys_category'; + $field = 'selected_categories'; + } + if (trim($record[$field]) === '') { + return ''; + } + $content = ''; + $uidList = explode(',', $record[$field]); + foreach ($uidList as $uid) { + $uid = (int)$uid; + $pageRecord = BackendUtility::getRecord($table, $uid, 'title'); + $content .= '<br>' . $pageRecord['title'] . ' (' . $uid . ')'; + } + return $content; + } + + /** + * Will create a link on the input string and possibly a big button after the string which links to editing in the RTE. + * Used for content element content displayed so the user can click the content / "Edit in Rich Text Editor" button + * + * @param string $linkText String to link. Must be prepared for HTML output. + * @param array $row The row. + * @return string If the whole thing was editable $str is return with link around. Otherwise just $str. + */ + protected function linkEditContent(string $linkText, $row): string + { + if ($this->getBackendUser()->recordEditAccessInternals('tt_content', $row)) { + $urlParameters = [ + 'edit' => [ + 'tt_content' => [ + $row['uid'] => 'edit' + ] + ], + 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') . '#element-tt_content-' . $row['uid'] + ]; + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters); + return '<a href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' . $linkText . '</a>'; + } + return $linkText; + } + + protected function createEmulatedPageLayoutViewFromDrawingConfiguration(DrawingConfiguration $drawingConfiguration): PageLayoutView + { + $pageLayoutView = GeneralUtility::makeInstance(PageLayoutView::class); + $pageLayoutView->option_newWizard = $drawingConfiguration->getShowNewContentWizard(); + $pageLayoutView->defLangBinding = $drawingConfiguration->getDefaultLanguageBinding(); + $pageLayoutView->tt_contentConfig['cols'] = implode(',', $drawingConfiguration->getActiveColumns()); + $pageLayoutView->tt_contentConfig['activeCols'] = implode(',', $drawingConfiguration->getActiveColumns()); + $pageLayoutView->tt_contentConfig['showHidden'] = $drawingConfiguration->getShowHidden(); + $pageLayoutView->tt_contentConfig['sys_language_uid'] = $drawingConfiguration->getLanguageColumnsPointer(); + if ($drawingConfiguration->getLanguageMode()) { + $pageLayoutView->tt_contentConfig['languageMode'] = 1; + $pageLayoutView->tt_contentConfig['languageCols'] = $drawingConfiguration->getLanguageColumns(); + $pageLayoutView->tt_contentConfig['languageColsPointer'] = $drawingConfiguration->getLanguageColumnsPointer(); + } + return $pageLayoutView; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getIconFactory(): IconFactory + { + return GeneralUtility::makeInstance(IconFactory::class); + } +} diff --git a/typo3/sysext/backend/Classes/Preview/StandardPreviewRendererResolver.php b/typo3/sysext/backend/Classes/Preview/StandardPreviewRendererResolver.php new file mode 100644 index 000000000000..073fd7f23690 --- /dev/null +++ b/typo3/sysext/backend/Classes/Preview/StandardPreviewRendererResolver.php @@ -0,0 +1,88 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Preview; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Class StandardPreviewRendererResolver + * + * Default implementation of PreviewRendererResolverInterface. + * Scans TCA configuration to detect: + * + * - TCA.$table.types.$typeFromTypeField.previewRenderer + * - TCA.$table.ctrl.previewRenderer + * + * Depending on which one is defined and checking the first, type-specific + * variant first. + */ +class StandardPreviewRendererResolver implements PreviewRendererResolverInterface +{ + /** + * @param string $table The name of the table the returned PreviewRenderer must work with + * @param array $row A record from $table which will be previewed - allows returning a different PreviewRenderer based on record attributes + * @param int $pageUid The UID of the page on which the preview will be rendered - allows returning a different PreviewRenderer based on for example pageTSconfig + * @return PreviewRendererInterface + * @throws \UnexpectedValueException + * @throws \RuntimeException + */ + public function resolveRendererFor(string $table, array $row, int $pageUid): PreviewRendererInterface + { + $tca = $GLOBALS['TCA'][$table]; + $tcaTypeField = $tca['ctrl']['type'] ?? null; + $previewRendererClassName = null; + if ($tcaTypeField) { + $tcaTypeOfRow = $row[$tcaTypeField]; + $typeConfiguration = $tca['types'][$tcaTypeOfRow] ?? []; + + $subTypeValueField = $typeConfiguration['subtype_value_field'] ?? null; + if (!empty($subTypeValueField) && !empty($typeConfiguration['previewRenderer']) && is_array($typeConfiguration['previewRenderer'])) { + // An array of subtype_value_field indexed preview renderers was defined, look up the right + // class to use for the sub-type defined in this $row. + $previewRendererClassName = $typeConfiguration['previewRenderer'][$row[$subTypeValueField]] ?? null; + } + + // If no class was found in the subtype_value_field + if (!$previewRendererClassName && !empty($typeConfiguration['previewRenderer'])) { + // A type-specific preview renderer was configured for the TCA type (and one was not detected + // based on the higher-priority lookups above). + $previewRendererClassName = $typeConfiguration['previewRenderer']; + } + } + + if (!$previewRendererClassName) { + + // Table either has no type field or no custom preview renderer was defined for the type. + // Use table's standard renderer if any is defined. + $previewRendererClassName = $tca['ctrl']['previewRenderer'] ?? null; + } + + if (!empty($previewRendererClassName)) { + if (!is_a($previewRendererClassName, PreviewRendererInterface::class, true)) { + throw new \UnexpectedValueException( + sprintf( + 'Class %s must implement %s', + $previewRendererClassName, + PreviewRendererInterface::class + ), + 1477512798 + ); + } + return GeneralUtility::makeInstance($previewRendererClassName); + } + throw new \RuntimeException(sprintf('No Preview renderer registered for table %s', $table), 1477520356); + } +} diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumnItem.php b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumnItem.php index 9dc2ad9d568f..dd39aa9200f0 100644 --- a/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumnItem.php +++ b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumnItem.php @@ -16,6 +16,7 @@ namespace TYPO3\CMS\Backend\View\BackendLayout\Grid; * The TYPO3 project - inspiring people to share! */ +use TYPO3\CMS\Backend\Preview\StandardPreviewRendererResolver; use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout; @@ -60,124 +61,16 @@ class GridColumnItem extends AbstractGridObject public function getPreview(): string { - $item = $this; - $row = $item->getRecord(); - $configuration = $this->backendLayout->getDrawingConfiguration(); - $out = ''; - $outHeader = ''; - - if ($row['header']) { - $hiddenHeaderNote = ''; - // If header layout is set to 'hidden', display an accordant note: - if ($row['header_layout'] == 100) { - $hiddenHeaderNote = ' <em>[' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden')) . ']</em>'; - } - $outHeader = $row['date'] - ? htmlspecialchars($configuration->getItemLabels()['date'] . ' ' . BackendUtility::date($row['date'])) . '<br />' - : ''; - $outHeader .= '<strong>' . $this->linkEditContent($this->renderText($row['header']), $row) - . $hiddenHeaderNote . '</strong><br />'; - } - - $drawItem = true; - - // Draw preview of the item depending on its CType (if not disabled by previous hook): - if ($drawItem) { - switch ($row['CType']) { - case 'header': - if ($row['subheader']) { - $out .= $this->linkEditContent($this->renderText($row['subheader']), $row) . '<br />'; - } - break; - case 'bullets': - case 'table': - if ($row['bodytext']) { - $out .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br />'; - } - break; - case 'uploads': - if ($row['media']) { - $out .= $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'media'), $row) . '<br />'; - } - break; - case 'shortcut': - if (!empty($row['records'])) { - $shortcutContent = []; - $recordList = explode(',', $row['records']); - foreach ($recordList as $recordIdentifier) { - $split = BackendUtility::splitTable_Uid($recordIdentifier); - $tableName = empty($split[0]) ? 'tt_content' : $split[0]; - $shortcutRecord = BackendUtility::getRecord($tableName, $split[1]); - if (is_array($shortcutRecord)) { - $icon = $this->iconFactory->getIconForRecord($tableName, $shortcutRecord, Icon::SIZE_SMALL)->render(); - $icon = BackendUtility::wrapClickMenuOnIcon( - $icon, - $tableName, - $shortcutRecord['uid'] - ); - $shortcutContent[] = $icon - . htmlspecialchars(BackendUtility::getRecordTitle($tableName, $shortcutRecord)); - } - } - $out .= implode('<br />', $shortcutContent) . '<br />'; - } - break; - case 'list': - $hookOut = ''; - $_params = ['pObj' => &$this, 'row' => $row, 'infoArr' => []]; - foreach ( - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'][$row['list_type']] ?? - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info']['_DEFAULT'] ?? - [] as $_funcRef - ) { - $hookOut .= GeneralUtility::callUserFunction($_funcRef, $_params, $this); - } - if ((string)$hookOut !== '') { - $out .= $hookOut; - } elseif (!empty($row['list_type'])) { - $label = BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'list_type', $row['list_type']); - if (!empty($label)) { - $out .= $this->linkEditContent('<strong>' . htmlspecialchars($this->getLanguageService()->sL($label)) . '</strong>', $row) . '<br />'; - } else { - $message = sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $row['list_type']); - $out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>'; - } - } else { - $out .= '<strong>' . $this->getLanguageService()->getLL('noPluginSelected') . '</strong>'; - } - $out .= htmlspecialchars($this->getLanguageService()->sL( - BackendUtility::getLabelFromItemlist('tt_content', 'pages', $row['pages']) - )) . '<br />'; - break; - default: - $contentType = $this->backendLayout->getDrawingConfiguration()->getContentTypeLabels()[$row['CType']]; - if (!isset($contentType)) { - $contentType = BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'CType', $row['CType']); - } - - if ($contentType) { - $out .= $this->linkEditContent('<strong>' . htmlspecialchars($contentType) . '</strong>', $row) . '<br />'; - if ($row['bodytext']) { - $out .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br />'; - } - if ($row['image']) { - $out .= $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'image'), $row) . '<br />'; - } - } else { - $message = sprintf( - $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), - $row['CType'] - ); - $out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>'; - } - } - } - $out = '<span class="exampleContent">' . $out . '</span>'; - $out = $outHeader . $out; - if ($item->isDisabled()) { - return '<span class="text-muted">' . $out . '</span>'; - } - return $out; + $record = $this->getRecord(); + $previewRenderer = GeneralUtility::makeInstance(StandardPreviewRendererResolver::class) + ->resolveRendererFor( + 'tt_content', + $record, + $this->backendLayout->getDrawingConfiguration()->getPageId() + ); + $previewHeader = $previewRenderer->renderPageModulePreviewHeader($this); + $previewContent = $previewRenderer->renderPageModulePreviewContent($this); + return $previewRenderer->wrapPageModulePreview($previewHeader, $previewContent, $this); } public function getWrapperClassName(): string @@ -222,16 +115,16 @@ class GridColumnItem extends AbstractGridObject return $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel'); } - public function getFooterInfo(): iterable + public function getFooterInfo(): string { - $info = []; - $this->getProcessedValue('starttime,endtime,fe_group,space_before_class,space_after_class', $info); - - if (!empty($GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']) && !empty($this->record[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']])) { - $info[] = $this->record[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']]; - } - - return $info; + $record = $this->getRecord(); + $previewRenderer = GeneralUtility::makeInstance(StandardPreviewRendererResolver::class) + ->resolveRendererFor( + 'tt_content', + $record, + $this->backendLayout->getDrawingConfiguration()->getPageId() + ); + return $previewRenderer->renderPageModulePreviewFooter($this); } /** @@ -253,70 +146,6 @@ class GridColumnItem extends AbstractGridObject return $title; } - /** - * Create thumbnail code for record/field but not linked - * - * @param mixed[] $row Record array - * @param string $table Table (record is from) - * @param string $field Field name for which thumbnail are to be rendered. - * @return string HTML for thumbnails, if any. - */ - protected function getThumbCodeUnlinked($row, $table, $field) - { - return BackendUtility::thumbCode($row, $table, $field, '', '', null, 0, '', '', false); - } - - /** - * Will create a link on the input string and possibly a big button after the string which links to editing in the RTE. - * Used for content element content displayed so the user can click the content / "Edit in Rich Text Editor" button - * - * @param string $str String to link. Must be prepared for HTML output. - * @param array $row The row. - * @return string If the whole thing was editable $str is return with link around. Otherwise just $str. - */ - public function linkEditContent($str, $row) - { - if ($this->getBackendUser()->recordEditAccessInternals('tt_content', $row)) { - $urlParameters = [ - 'edit' => [ - 'tt_content' => [ - $row['uid'] => 'edit' - ] - ], - 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') . '#element-tt_content-' . $row['uid'] - ]; - $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); - $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters); - return '<a href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' . $str . '</a>'; - } - return $str; - } - - /** - * Processing of larger amounts of text (usually from RTE/bodytext fields) with word wrapping etc. - * - * @param string $input Input string - * @return string Output string - */ - public function renderText($input): string - { - $input = strip_tags($input); - $input = GeneralUtility::fixed_lgd_cs($input, 1500); - return nl2br(htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8', false)); - } - - protected function getProcessedValue(string $fieldList, array &$info): void - { - $itemLabels = $this->backendLayout->getDrawingConfiguration()->getItemLabels(); - $fieldArr = explode(',', $fieldList); - foreach ($fieldArr as $field) { - if ($this->record[$field]) { - $info[] = '<strong>' . htmlspecialchars($itemLabels[$field]) . '</strong> ' - . htmlspecialchars(BackendUtility::getProcessedValue('tt_content', $field, $this->record[$field])); - } - } - } - public function getIcons(): string { $table = 'tt_content'; diff --git a/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Record/Footer.html b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Record/Footer.html index c0b4c95e89c8..2f6108320401 100644 --- a/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Record/Footer.html +++ b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Record/Footer.html @@ -1,7 +1,7 @@ -<div class="t3-page-ce-footer"> - <div class="t3-page-ce-info"> - <f:for each="{item.footerInfo}" as="infoLine" iteration="iteration"> - {infoLine -> f:format.raw()}<f:if condition="!{iteration.isLast}"><br /></f:if> - </f:for> +<f:if condition="{item.footerInfo}"> + <div class="t3-page-ce-footer"> + <div class="t3-page-ce-info"> + {item.footerInfo -> f:format.raw()} + </div> </div> -</div> +</f:if> diff --git a/typo3/sysext/backend/ext_localconf.php b/typo3/sysext/backend/ext_localconf.php index d3c15687601b..d592adfc82ae 100644 --- a/typo3/sysext/backend/ext_localconf.php +++ b/typo3/sysext/backend/ext_localconf.php @@ -28,6 +28,11 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1460321142] = [ // Register search key shortcuts $GLOBALS['TYPO3_CONF_VARS']['SYS']['livesearch']['page'] = 'pages'; +// Register standard preview renderer resolver implementation. +// Resolves PreviewRendererInterface implementations for a given table and record. +// Can be replaced with custom implementation by overriding this value in extensions. +$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['previewRendererResolver'] = \TYPO3\CMS\Backend\Preview\StandardPreviewRendererResolver::class; + // Include base TSconfig setup \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig( "@import 'EXT:backend/Configuration/TSconfig/Page/Mod/Wizards/NewContentElement.tsconfig'" diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-90348-PageLayoutViewClassInternalIsDeprecated.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-90348-PageLayoutViewClassInternalIsDeprecated.rst index 140c4d24f473..b3453af73054 100644 --- a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-90348-PageLayoutViewClassInternalIsDeprecated.rst +++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-90348-PageLayoutViewClassInternalIsDeprecated.rst @@ -21,8 +21,10 @@ Implementations which depend on :php:`PageLayoutView` should prepare to use the Affected Installations ====================== -Any site which uses PSR-14 events or backend content rendering hooks associated with :php:`PageLayoutView` such as :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter']`. - +* Any site which overrides the ``PageLayoutView`` class. The overridden class will still be instanced when rendering previews in BE page module - but no methods will be called on the instance **unless** they are called by a third party hook subscriber. +* Any site which depends on PSR-14 events associated with ``PageLayoutView`` will only have those events dispatched if the ``fluidBasedPageModule`` feature flag is ``false``. + * Affects ``\TYPO3\CMS\Backend\View\Event\AfterSectionMarkupGeneratedEvent``. + * Affects ``\TYPO3\CMS\Backend\View\Event\BeforeSectionMarkupGeneratedEvent``. Migration ========= diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-78450-IntroducePreviewRendererPattern.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-78450-IntroducePreviewRendererPattern.rst new file mode 100644 index 000000000000..8d768c394b1a --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-78450-IntroducePreviewRendererPattern.rst @@ -0,0 +1,158 @@ +.. include:: ../../Includes.txt + +=================================================== +Feature: #78450 - Introduce PreviewRenderer pattern +=================================================== + +See :issue:`78450` + +Pre-requisites +============== + +The PreviewRenderer usage is only active if the "fluid based page layout module" feature is enabled. This feature +is enabled by default in TYPO3 versions 10.3 and later. + +The feature toggle can be located in the `Settings` admin module under `Feature Toggles`. Or it can be set in +PHP using :php:``$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['fluidBasedPageModule'] = true;``. + + +Description +=========== + +A new pattern has been introduced to facilitate (record) previews in TYPO3. A default implementation has been +added which provides support for the previous methods of generating previews (content previews - using hooks +or by defining a Fluid template to render). + +The new pattern creates a strict contract for code which generates such previews and enables switching out the +implementation of both the resolving logic (which finds a preview renderer for a given table and record) as well +as the rendering logic (which now renders both the actual preview and has contract methods for adding wrapping). + +The main differences between the old and the new approach are: + +* The class used to render previews is now defined in TCA and can be defined per-type or for any type. +* The resolver used to find preview renderers is a global implementation overridable in configuration. +* A single preview renderer will now be used; before, hook subscribers had to toggle passed-by-reference flags. +* Wrapping is no longer forced to be a `<span>` tag so you are not restricted to inline and inline-block display. +* Preview renderers have a public contract which splits up actual preview and wrapping, allowing third party renderers + to subclass the original renderer and for example only change the wrapping tag. +* Preview rendering can now be done ad-hoc through; the pattern can be used from any context where the old pattern + could only be used (was only used) in the PageLayoutView for content previews. + + +Impact +====== + +The feature adds two new concepts: + +* `PreviewRendererResolver` which is a global implementation to detect which `PreviewRenderer` a given record needs. +* `PreviewRenderer` which is the class responsible for generating the preview and the wrapping. + + +Configuring the implementation +------------------------------ + +The PreviewRendererResolver can be overridden by setting: + +.. code-block:: php + + $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['previewRendererResolver'] = \TYPO3\CMS\Backend\Preview\StandardPreviewRendererResolver::class; + + +(the class shown is the standard implementation TYPO3 provides, inspect this class for further developer information) + +Once overridden the old resolver will no longer be consulted. + +And individual preview renderers can be defined using one of the following two approaches: + +.. code-block:: php + + $GLOBALS['TCA'][$table]['ctrl']['previewRenderer'] = My\PreviewRenderer::class; + + +Which specifies the PreviewRenderer to use for any record in `$table` + +Or if your table has a "type" field/attribute: + +.. code-block:: php + + $GLOBALS['TCA'][$table]['types'][$type]['previewRenderer'] = My\PreviewRenderer::class; + +Which specifies the PreviewRenderer for only records of type `$type` as determined by the type field of your table. + +Or finally, if your table and field has a `subtype_value_field` TCA setting (like `tt_content.list_type` for example) +and you want register a preview renderer that applies only when that value is selected (e.g. when a certain plugin type +is selected and you can't match it with the "type" of the record alone): + +.. code-block:: php + + $GLOBALS['TCA'][$table]['types'][$type]['previewRenderer'][$subType] = My\PreviewRenderer::class; + +Where `$type` is for example `list` (indicating a plugin) and `$subType` is the value of the `list_type` field when the +type of plugin you want to target, is selected as plugin type. + +Note: recommended location is in the `ctrl` array in your extension's `Configuration/TCA/$table.php` or +`Configuration/TCA/Overrides/$table.php` file. The former is used when your extension is the one that creates the table - +the latter is used when you need to override TCA properties of tables added by the core or other extensions. + + +The PreviewRenderer interface +----------------------------- + +`\TYPO3\CMS\Backend\Preview\PreviewRendererResolverInterface` must be implemented by PreviewRendererResolvers and +contains a single API method, `public function resolveRendererFor($table, array $row, int $pageUid);` which +unsurprisingly returns a single PreviewRenderer based on the given input. + +`\TYPO3\CMS\Backend\Preview\PreviewRendererInterface` must be implemented by any PreviewRenderer and contains a few +API methods: + +.. code-block:: php + /** + * Dedicated method for rendering preview header HTML for + * the page module only. Receives $item which is an instance + * GridColumnItem which has a getter method to return the record. + * + * @param GridColumnItem + * @return string + */ + public function renderPageModulePreviewHeader(GridColumnItem $item); + + /** + * Dedicated method for rendering preview body HTML for + * the page module only. + * + * @param GridColumnItem $item + * @return string + */ + public function renderPageModulePreviewContent(GridColumnItem $item); + + /** + * Render a footer for the record to display in page module below + * the body of the item's preview. + * + * @param GridColumnItem $item + * @return string + */ + public function renderPageModulePreviewFooter(GridColumnItem $item): string; + + /** + * Dedicated method for wrapping a preview header and body HTML. + * + * @param string $previewHeader + * @param string $previewContent + * @param GridColumnItem $item + * @return string + */ + public function wrapPageModulePreview($previewHeader, $previewContent, GridColumnItem $item); + +With further methods expected to be added to support generic preview rendering, e.g. usages outside PageLayoutView. +Implementing these methods allows you to control the exact composition of the preview. + +This means assuming your PreviewRenderer returns `<h4>Header</h4>` from the header render method and `<p>Body</p>` from +the preview content rendering method and your wrapping method does `return '<div>' . $previewHeader . $previewContent . '</div>';` then the +entire output becomes `<div><h4>Header</h4><p>Body</p></div>` when combined. + +Should you wish to reuse parts of the default preview rendering and only change, for example, the method that renders +the preview body content, you can subclass ``\TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer`` in your +own PreviewRenderer class - and selectively override the methods from the API displayed above. + +.. index:: Backend, TCA diff --git a/typo3/sysext/frontend/Configuration/TCA/tt_content.php b/typo3/sysext/frontend/Configuration/TCA/tt_content.php index d8b60475de21..01ad1774377e 100644 --- a/typo3/sysext/frontend/Configuration/TCA/tt_content.php +++ b/typo3/sysext/frontend/Configuration/TCA/tt_content.php @@ -23,6 +23,7 @@ return [ 'transOrigDiffSourceField' => 'l18n_diffsource', 'languageField' => 'sys_language_uid', 'translationSource' => 'l10n_source', + 'previewRenderer' => \TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer::class, 'enablecolumns' => [ 'disabled' => 'hidden', 'starttime' => 'starttime', -- GitLab