From dd1183071704c3ea591a36a3a2f1d0b7f9497cdf Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Mon, 5 Aug 2024 15:59:16 +0200 Subject: [PATCH] refactor: frontend editing with advanced content element information --- Classes/Controller/EditController.php | 193 +++++++++++++++--- .../FrontendEditingDropdownModifyEvent.php | 25 +++ Configuration/Backend/Routes.php | 11 +- Configuration/TypoScript/setup.typoscript | 3 - Resources/Private/Language/locallang.xlf | 24 ++- Resources/Public/Css/FrontendEditing.css | 39 +++- .../Public/JavaScript/frontend_editing.js | 90 +++++--- 7 files changed, 298 insertions(+), 87 deletions(-) create mode 100644 Classes/Event/FrontendEditingDropdownModifyEvent.php diff --git a/Classes/Controller/EditController.php b/Classes/Controller/EditController.php index 5382869..4b47b16 100644 --- a/Classes/Controller/EditController.php +++ b/Classes/Controller/EditController.php @@ -7,45 +7,176 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Backend\Routing\UriBuilder; -use TYPO3\CMS\Core\Http\RedirectResponse; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Core\Bootstrap; +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\EventDispatcher\EventDispatcher; +use TYPO3\CMS\Core\Http\JsonResponse; +use TYPO3\CMS\Core\Imaging\IconFactory; +use TYPO3\CMS\Core\Type\Bitmask\Permission; use TYPO3\CMS\Core\Utility\GeneralUtility; +use Xima\XimaTypo3Toolbox\Event\FrontendEditingDropdownModifyEvent; final class EditController { - public function editContentElement(ServerRequestInterface $request): ResponseInterface + public function editableContentElementsByPage(ServerRequestInterface $request): ResponseInterface { - $routing = $request->getAttribute('routing'); - $id = $routing['identifier']; - - $returnUrl = urldecode($request->getQueryParams()['returnUrl']); - $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); - $uri = $uriBuilder->buildUriFromRoute( - 'record_edit', - [ - 'edit' => [ - 'tt_content' => [ - $id => 'edit', + $pid = $request->getQueryParams()['pid'] + ?? throw new \InvalidArgumentException( + 'Please provide pid', + 1722599959, + ); + $returnUrl = $request->getQueryParams()['returnUrl'] ? strtok(urldecode($request->getQueryParams()['returnUrl']), '#') : ''; + $language_uid = $request->getQueryParams()['language_uid'] ?? 0; + + /* @var $backendUser \TYPO3\CMS\Core\Authentication\BackendUserAuthentication */ + $backendUser = $GLOBALS['BE_USER']; + if ($backendUser->user === null) { + Bootstrap::initializeBackendAuthentication(); + $backendUser->initializeUserSessionManager(); + $backendUser = $GLOBALS['BE_USER']; + } + + if (!BackendUtility::readPageAccess( + $pid, + $backendUser->getPagePermsClause(Permission::PAGE_SHOW) + )) { + return new JsonResponse([]); + } + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); + + $contentElements = $queryBuilder + ->select('*') + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq('hidden', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)), + $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)), + $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)), + $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter($language_uid, Connection::PARAM_INT)), + ) + ->executeQuery()->fetchAllAssociative(); + + $iconFactory = GeneralUtility::makeInstance(IconFactory::class); + + $result = []; + foreach ($contentElements as $contentElement) { + // ToDo: is this sufficient? + if (!$backendUser->recordEditAccessInternals('tt_content', $contentElement['uid'])) { + continue; + } + + $contentElementConfig = $this->getContentElementConfig($contentElement['CType'], $contentElement['list_type']); + $result[$contentElement['uid']] = [ + 'uid' => $contentElement['uid'], + 'type' => $contentElement['CType'], + 'label' => $GLOBALS['LANG']->sL('LLL:EXT:xima_typo3_toolbox/Resources/Private/Language/locallang.xlf:editMenu'), + 'icon' => $iconFactory->getIcon('actions-open', 'small')->getAlternativeMarkup('inline'), + 'actions' => [ + 'intro' => [ + 'type' => 'header', + 'label' => $GLOBALS['LANG']->sL($contentElementConfig['label']), + 'icon' => $iconFactory->getIcon($contentElementConfig['icon'], 'small')->getAlternativeMarkup('inline'), + ], + 'div_edit' => [ + 'type' => 'divider', + 'label' => $GLOBALS['LANG']->sL('LLL:EXT:xima_typo3_toolbox/Resources/Private/Language/locallang.xlf:divEdit'), + ], + 'edit' => [ + 'type' => 'link', + 'label' => $contentElement['CType'] === 'list' ? $GLOBALS['LANG']->sL('LLL:EXT:xima_typo3_toolbox/Resources/Private/Language/locallang.xlf:editPlugin') : $GLOBALS['LANG']->sL('LLL:EXT:xima_typo3_toolbox/Resources/Private/Language/locallang.xlf:editContentElement'), + 'icon' => $iconFactory->getIcon($contentElement['CType'] === 'list' ? 'content-plugin' : 'content-textpic', 'small')->getAlternativeMarkup('inline'), + 'url' => GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute( + 'record_edit', + [ + 'edit' => [ + 'tt_content' => [ + $contentElement['uid'] => 'edit', + ], + ], + 'returnUrl' => $returnUrl . '#c' . $contentElement['uid'], + ], + )->__toString(), + ], + 'page' => [ + 'type' => 'link', + 'label' => $GLOBALS['LANG']->sL('LLL:EXT:xima_typo3_toolbox/Resources/Private/Language/locallang.xlf:editPage'), + 'icon' => $iconFactory->getIcon('apps-pagetree-page-default', 'small')->getAlternativeMarkup('inline'), + 'url' => GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute( + 'web_layout', + [ + 'id' => $pid, + 'returnUrl' => $returnUrl . '#c' . $contentElement['uid'], + ], + )->__toString(), + ], + 'div_action' => [ + 'type' => 'divider', + 'label' => $GLOBALS['LANG']->sL('LLL:EXT:xima_typo3_toolbox/Resources/Private/Language/locallang.xlf:divAction'), + ], + 'hide' => [ + 'type' => 'link', + 'label' => $GLOBALS['LANG']->sL('LLL:EXT:xima_typo3_toolbox/Resources/Private/Language/locallang.xlf:hide'), + 'icon' => $iconFactory->getIcon('actions-toggle-on', 'small')->getAlternativeMarkup('inline'), + 'url' => GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute( + 'tce_db', + [ + 'data' => [ + 'tt_content' => [ + $contentElement['uid'] => [ + 'hidden' => 1, + ], + ], + ], + 'returnUrl' => $returnUrl . '#c' . $contentElement['uid'], + ], + )->__toString(), + ], + 'info' => [ + 'type' => 'link', + 'label' => $GLOBALS['LANG']->sL('LLL:EXT:xima_typo3_toolbox/Resources/Private/Language/locallang.xlf:info'), + 'icon' => $iconFactory->getIcon('actions-info', 'small')->getAlternativeMarkup('inline'), + 'url' => GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute( + 'show_item', + [ + 'uid' => $contentElement['uid'], + 'table' => 'tt_content', + 'returnUrl' => $returnUrl . '#c' . $contentElement['uid'], + ], + )->__toString(), + ], + 'history' => [ + 'type' => 'link', + 'label' => $GLOBALS['LANG']->sL('LLL:EXT:xima_typo3_toolbox/Resources/Private/Language/locallang.xlf:history'), + 'icon' => $iconFactory->getIcon('actions-history', 'small')->getAlternativeMarkup('inline'), + 'url' => GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute( + 'record_history', + [ + 'element' => 'tt_content:' . $contentElement['uid'], + 'returnUrl' => $returnUrl . '#c' . $contentElement['uid'], + ], + )->__toString(), ], ], - 'returnUrl' => $returnUrl, - ], - ); - return new RedirectResponse($uri); + ]; + } + + GeneralUtility::makeInstance(EventDispatcher::class)->dispatch(new FrontendEditingDropdownModifyEvent($result)); + + return new JsonResponse($result); } - public function editPage(ServerRequestInterface $request): ResponseInterface + + private function getContentElementConfig(string $cType, string $listType): array|bool { - $routing = $request->getAttribute('routing'); - $pid = $routing['identifier']; - $returnUrl = urldecode($request->getQueryParams()['returnUrl']); - - $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); - $uri = $uriBuilder->buildUriFromRoute( - 'web_layout', - [ - 'id' => $pid, - 'returnUrl' => $returnUrl, - ], - ); - return new RedirectResponse($uri); + $tca = $cType === 'list' ? $GLOBALS['TCA']['tt_content']['columns']['list_type']['config']['items'] : $GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items']; + + foreach ($tca as $item) { + if (($cType === 'list' && $item['value'] === $listType) || $item['value'] === $cType) { + return $item; + } + } + + return false; } } diff --git a/Classes/Event/FrontendEditingDropdownModifyEvent.php b/Classes/Event/FrontendEditingDropdownModifyEvent.php new file mode 100644 index 0000000..563c6c3 --- /dev/null +++ b/Classes/Event/FrontendEditingDropdownModifyEvent.php @@ -0,0 +1,25 @@ +dropdownData; + } + + public function setDropdownData(array $dropdownData): void + { + $this->dropdownData = $dropdownData; + } +} diff --git a/Configuration/Backend/Routes.php b/Configuration/Backend/Routes.php index 50a87bc..831e0ed 100644 --- a/Configuration/Backend/Routes.php +++ b/Configuration/Backend/Routes.php @@ -1,14 +1,9 @@ [ - 'path' => '/edit-content-element-redirect/{identifier}', + 'editable_content_elements' => [ + 'path' => '/editable-content-elements', 'access' => 'public', - 'target' => \Xima\XimaTypo3Toolbox\Controller\EditController::class . '::editContentElement', - ], - 'edit_page_redirect' => [ - 'path' => '/edit-page-redirect/{identifier}', - 'access' => 'public', - 'target' => \Xima\XimaTypo3Toolbox\Controller\EditController::class . '::editPage', + 'target' => \Xima\XimaTypo3Toolbox\Controller\EditController::class . '::editableContentElementsByPage', ], ]; diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.typoscript index be4a2d4..3756008 100644 --- a/Configuration/TypoScript/setup.typoscript +++ b/Configuration/TypoScript/setup.typoscript @@ -28,9 +28,6 @@ page.includeJS { customFrontendEditing = EXT:xima_typo3_toolbox/Resources/Public/JavaScript/frontend_editing.js } - page.inlineLanguageLabelFiles { - customFrontendEditing = EXT:xima_typo3_toolbox/Resources/Private/Language/locallang.xlf - } page.jsInline { 1722340759 = TEXT 1722340759.stdWrap.dataWrap = const pid = {TSFE:id}; diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index db0a98b..d2b02cb 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -6,15 +6,33 @@ Last Update - + Edit menu - + Edit content element - + + Edit plugin + + Edit page + + History + + + Info + + + Hide + + + Edit + + + Action + Technical headline diff --git a/Resources/Public/Css/FrontendEditing.css b/Resources/Public/Css/FrontendEditing.css index 3edef0c..a5d1933 100644 --- a/Resources/Public/Css/FrontendEditing.css +++ b/Resources/Public/Css/FrontendEditing.css @@ -15,6 +15,10 @@ padding: 0; } +.xima-typo3-toolbox--edit-button svg { + width: 10px; +} + .xima-typo3-toolbox--edit-button:hover { opacity: 1; box-shadow: 0 0 1px 1px rgba(0, 0, 0, .1); @@ -24,6 +28,9 @@ position: relative; } +.xima-typo3-toolbox--edit-container:hover { + outline: 1px dashed gray; +} .xima-typo3-toolbox--dropdown-menu { position: absolute; display: none; @@ -37,10 +44,12 @@ border-radius: .75rem; box-shadow: 0 0 1px 1px rgba(0, 0, 0, .1); z-index: 1000; + direction: ltr; } -.xima-typo3-toolbox--dropdown-menu a { - display: block; +.xima-typo3-toolbox--dropdown-menu a, .xima-typo3-toolbox--dropdown-menu div { + display: flex; + gap: 5px; padding: 10px; color: #333; text-decoration: none; @@ -54,18 +63,32 @@ font-weight: normal; } -.xima-typo3-toolbox--dropdown-menu a:hover { - background-color: #f0f0f0; +.xima-typo3-toolbox--dropdown-menu div { + border-radius: 0; } -*[id*="c"]:hover { - outline: 1px dashed gray; +.xima-typo3-toolbox--dropdown-menu div.xima-typo3-toolbox--divider { + color: #666; + text-transform: uppercase; + font-size: 10px; + border-bottom: 1px solid rgba(0, 0, 0, .1); + padding: 5px; } -*[id*="c"] .xima-typo3-toolbox--edit-button { +.xima-typo3-toolbox--dropdown-menu a svg, .xima-typo3-toolbox--dropdown-menu div svg { + width: 16px; +} + +.xima-typo3-toolbox--dropdown-menu a:hover { + background-color: #f0f0f0; +} + +.xima-typo3-toolbox--edit-container .xima-typo3-toolbox--edit-button { visibility: hidden; } -*[id*="c"]:hover .xima-typo3-toolbox--edit-button { +.xima-typo3-toolbox--edit-container:hover .xima-typo3-toolbox--edit-button { visibility: visible; } + + diff --git a/Resources/Public/JavaScript/frontend_editing.js b/Resources/Public/JavaScript/frontend_editing.js index c8c1c95..7f78480 100644 --- a/Resources/Public/JavaScript/frontend_editing.js +++ b/Resources/Public/JavaScript/frontend_editing.js @@ -1,46 +1,68 @@ document.addEventListener('DOMContentLoaded', function() { - const elements = document.querySelectorAll('[id^="c"]'); + const getContentElements = async () => { + const url = encodeURIComponent(window.location.href); + const endpoint = `/typo3/editable-content-elements?pid=${pid}&returnUrl=${url}`; - elements.forEach(function(element) { - element.classList.add('xima-typo3-toolbox--edit-container'); - const id = element.id.slice(1); - if (isNaN(id)) { - return; - } + try { + const response = await fetch(endpoint, { cache: 'no-cache' }); + if (!response.ok) return; + + const jsonResponse = await response.json(); + + for (let uid in jsonResponse) { + const contentElement = jsonResponse[uid]; + const element = document.querySelector(`#c${uid}`); + if (!element) continue; - const url = encodeURIComponent(window.location.href + '#c' + id); + element.classList.add('xima-typo3-toolbox--edit-container'); - let editButton = document.createElement('button'); - editButton.className = 'xima-typo3-toolbox--edit-button'; - editButton.title = TYPO3.lang['frontend-editing.label.menu']; - editButton.innerHTML = '\n'; + const editButton = document.createElement('button'); + editButton.className = 'xima-typo3-toolbox--edit-button'; + editButton.title = contentElement.label; + editButton.innerHTML = contentElement.icon; - let dropdownMenu = document.createElement('div'); - dropdownMenu.className = 'xima-typo3-toolbox--dropdown-menu'; + const dropdownMenu = document.createElement('div'); + dropdownMenu.className = 'xima-typo3-toolbox--dropdown-menu'; - let editContentElementLink = document.createElement('a'); - editContentElementLink.href = '/typo3/edit-content-element-redirect/' + id + '?returnUrl=' + url; - editContentElementLink.innerHTML = ' ' + TYPO3.lang['frontend-editing.label.edit-content-element']; + for (let actionName in contentElement.actions) { + const action = contentElement.actions[actionName]; + let actionElement; - let editPageLink = document.createElement('a'); - editPageLink.href = '/typo3/edit-page-redirect/' + pid + '?returnUrl=' + url; - editPageLink.innerHTML = ' ' + TYPO3.lang['frontend-editing.label.edit-page']; + if (action.type === 'link') { + actionElement = document.createElement('a'); + actionElement.href = action.url; + } else if (action.type === 'divider') { + actionElement = document.createElement('div'); + actionElement.className = 'xima-typo3-toolbox--divider'; + } else { + actionElement = document.createElement('div'); + } - dropdownMenu.appendChild(editContentElementLink); - dropdownMenu.appendChild(editPageLink); + actionElement.innerHTML = `${action.icon ?? ''} ${action.label}`; + dropdownMenu.appendChild(actionElement); + } - editButton.addEventListener('click', function(event) { - event.preventDefault(); - dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block'; - }); + editButton.addEventListener('click', function(event) { + event.preventDefault(); + dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block'; + }); - element.appendChild(editButton); - element.appendChild(dropdownMenu); - }); + element.appendChild(editButton); + element.appendChild(dropdownMenu); + + document.addEventListener('click', function(event) { + document.querySelectorAll('.xima-typo3-toolbox--dropdown-menu').forEach(function(menu) { + const button = menu.previousElementSibling; + if (!menu.contains(event.target) && !button.contains(event.target)) { + menu.style.display = 'none'; + } + }); + }); + } + } catch (error) { + console.log(error); + } + }; - document.addEventListener('click', function(event) { - document.querySelectorAll('.xima-typo3-toolbox--dropdown-menu').forEach(function(dropdownMenu) { - dropdownMenu.parentNode.contains(event.target) ? dropdownMenu.style.display = 'block' : dropdownMenu.style.display = 'none'; - }); - }); + getContentElements(); });