├── ext_conf_template.txt
├── Resources
├── Public
│ ├── Screenshots
│ │ ├── screenshot1.png
│ │ └── usersettings.png
│ ├── Icons
│ │ ├── pen-solid.svg
│ │ └── Extension.svg
│ ├── JavaScript
│ │ └── popover.js
│ └── Styles
│ │ └── basic.css
└── Private
│ └── Language
│ └── locallang_adminpanel.xlf
├── ext_tables.php
├── feedback.md
├── Configuration
├── RequestMiddlewares.php
└── Services.yaml
├── ext_emconf.php
├── composer.json
├── Classes
├── Event
│ └── EditPanelActionEvent.php
├── Configuration.php
├── Middleware
│ └── FrontendEditInitiator.php
├── ContentObject
│ └── EditableFluidTemplateContentObject.php
├── ViewHelpers
│ └── PanelViewHelper.php
├── Service
│ └── AccessService.php
├── Edit
│ ├── Permissions.php
│ └── EditPanel.php
├── Utility
│ └── ContentElementOrder.php
└── EventListener
│ └── DefaultEditPanelActionEventListener.php
└── Readme.md
/ext_conf_template.txt:
--------------------------------------------------------------------------------
1 | # cat=basic/enable/20; type=string; label=How enabled for users
2 | renderCheck = usersettings
--------------------------------------------------------------------------------
/Resources/Public/Screenshots/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/georgringer/feediting/HEAD/Resources/Public/Screenshots/screenshot1.png
--------------------------------------------------------------------------------
/Resources/Public/Screenshots/usersettings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/georgringer/feediting/HEAD/Resources/Public/Screenshots/usersettings.png
--------------------------------------------------------------------------------
/ext_tables.php:
--------------------------------------------------------------------------------
1 | 'Enable FE Editing',
6 | 'type' => 'check',
7 | ];
8 | if (!isset($GLOBALS['TYPO3_USER_SETTINGS']['showitem'])) {
9 | $GLOBALS['TYPO3_USER_SETTINGS']['showitem'] = '';
10 | }
11 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addFieldsToUserSettings('--div--;EXT:feediting,feediting');
12 |
13 |
--------------------------------------------------------------------------------
/Resources/Private/Language/locallang_adminpanel.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Feedit
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/feedback.md:
--------------------------------------------------------------------------------
1 | # I need feedback!
2 |
3 | This extension is currently in development and I need feedback - especially from editors and integrators!
4 |
5 | I would like answers to the following questions:
6 |
7 | - Are you editor, developer, integrator?
8 | - Is this extension useful to you?
9 | - What is missing? E.g. specific actions in the toolbar
10 | - Additional comments
11 |
12 | Please report at https://github.com/georgringer/feediting/issues or via slack, mail, phone!
13 |
14 | Thanks a lot!
--------------------------------------------------------------------------------
/Configuration/RequestMiddlewares.php:
--------------------------------------------------------------------------------
1 | [
5 | 'typo3/cms-frontendedit/initiator' => [
6 | 'target' => \GeorgRinger\Feediting\Middleware\FrontendEditInitiator::class,
7 | 'after' => [
8 | 'typo3/cms-adminpanel/initiator',
9 | 'typo3/cms-frontend/page-resolver',
10 | ],
11 | 'before' => [
12 | 'typo3/cms-frontend/prepare-tsfe-rendering',
13 | ],
14 | ],
15 | ]
16 | ];
17 |
--------------------------------------------------------------------------------
/Resources/Public/Icons/pen-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ext_emconf.php:
--------------------------------------------------------------------------------
1 | 'Feediting',
5 | 'description' => '',
6 | 'category' => 'fe',
7 | 'author' => 'Georg Ringer',
8 | 'author_email' => 'mail@ringer.it',
9 | 'state' => 'alpha',
10 | 'version' => '0.0.1',
11 | 'constraints' => [
12 | 'depends' => [
13 | 'typo3' => '12.4.0-12.4.99',
14 | 'backend' => '12.4.0-12.4.99',
15 | 'frontend' => '12.4.0-12.4.99',
16 | ],
17 | 'conflicts' => [],
18 | 'suggests' => [],
19 | ],
20 | ];
21 |
--------------------------------------------------------------------------------
/Configuration/Services.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | _defaults:
3 | autowire: true
4 | autoconfigure: true
5 | public: false
6 |
7 | GeorgRinger\Feediting\:
8 | resource: '../Classes/*'
9 |
10 | GeorgRinger\Feediting\Edit\Clipboard:
11 | public: true
12 |
13 | # GeorgRinger\Feediting\Event\EditPanelActionEvent:
14 | # public: true
15 |
16 | GeorgRinger\Feediting\ContentObject\EditableFluidTemplateContentObject:
17 | tags:
18 | - name: frontend.contentobject
19 | identifier: 'FLUIDTEMPLATE'
20 |
21 | GeorgRinger\Feediting\EventListener\DefaultEditPanelActionEventListener:
22 | tags:
23 | - name: event.listener
24 | identifier: 'tx_feediting_defaulteditpanelactioneventlistener'
25 | event: GeorgRinger\Feediting\Event\EditPanelActionEvent
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "georgringer/feediting",
3 | "type": "typo3-cms-extension",
4 | "authors": [
5 | {
6 | "name": "Georg Ringer",
7 | "role": "Developer"
8 | }
9 | ],
10 | "require": {
11 | "php": "^8.2",
12 | "typo3/cms-backend": "^12.4",
13 | "typo3/cms-beuser": "^12.4",
14 | "typo3/cms-core": "^12.4",
15 | "typo3/cms-frontend": "^12.4"
16 | },
17 | "suggest": {
18 | },
19 | "license": [
20 | "GPL-2.0-or-later"
21 | ],
22 | "autoload": {
23 | "psr-4": {
24 | "GeorgRinger\\Feediting\\": "Classes"
25 | }
26 | },
27 | "extra": {
28 | "typo3/cms": {
29 | "extension-key": "feediting"
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Classes/Event/EditPanelActionEvent.php:
--------------------------------------------------------------------------------
1 | actions;
25 | }
26 |
27 | public function setActions(array $actions): void
28 | {
29 | $this->actions = $actions;
30 | }
31 |
32 | public function addAction(string $action): void
33 | {
34 | $this->actions[] = $action;
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/Classes/Configuration.php:
--------------------------------------------------------------------------------
1 | get('feediting');
18 |
19 | $renderCheck = strtolower($extensionSettings['renderCheck'] ?? '');
20 | if ($renderCheck === 'adminpanel' && ExtensionManagementUtility::isLoaded('admin_panel')) {
21 | $this->renderCheck = 'adminpanel';
22 | } else {
23 | $this->renderCheck = $renderCheck;
24 | }
25 | }
26 |
27 | public function getRenderCheck(): string
28 | {
29 | return $this->renderCheck;
30 | }
31 | }
--------------------------------------------------------------------------------
/Classes/Middleware/FrontendEditInitiator.php:
--------------------------------------------------------------------------------
1 | enabled()) {
21 | $GLOBALS['TSFE']->set_no_cache('EXT:feediting in action', true);
22 | }
23 | }
24 | return $handler->handle($request);
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Classes/ContentObject/EditableFluidTemplateContentObject.php:
--------------------------------------------------------------------------------
1 | cObj->currentRecord);
16 | $tableName = $split[0] ?? '';
17 | $recordId = (int)($split[1] ?? 0);
18 | $content = parent::render($conf);
19 |
20 | if (!$tableName || !$recordId) {
21 | return $content;
22 | }
23 |
24 | $editPanel = GeneralUtility::makeInstance(EditPanel::class, $this->request, $tableName, (int)$recordId, $this->cObj->data);
25 | $contentWithEditPanel = $editPanel->render($content);
26 | if (empty($contentWithEditPanel)) {
27 | return $content;
28 | }
29 |
30 | return $contentWithEditPanel;
31 | }
32 |
33 | }
--------------------------------------------------------------------------------
/Resources/Public/Icons/Extension.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Classes/ViewHelpers/PanelViewHelper.php:
--------------------------------------------------------------------------------
1 | registerArgument('table', 'string', 'The table to be edited', true);
18 | $this->registerArgument('uid', 'int', 'Id', true);
19 | }
20 |
21 | public function render(): string
22 | {
23 | $recordId = (int)$this->arguments['uid'];
24 | $tableName = $this->arguments['table'];
25 | $row = BackendUtility::getRecord($tableName, $recordId);
26 | if (!$row) {
27 | return '';
28 | }
29 |
30 | $editPanel = GeneralUtility::makeInstance(EditPanel::class, $this->renderingContext->getRequest(), $tableName, $recordId, $row);
31 | $contentWithEditPanel = $editPanel->render('');
32 | if (empty($contentWithEditPanel)) {
33 | return '';
34 | }
35 | return $contentWithEditPanel;
36 | }
37 | }
--------------------------------------------------------------------------------
/Classes/Service/AccessService.php:
--------------------------------------------------------------------------------
1 | configuration = new Configuration();
18 | }
19 |
20 | public function enabled(): bool
21 | {
22 | $beUser = $this->getBackendUser();
23 | if (!$beUser) {
24 | return false;
25 | }
26 |
27 | switch ($this->configuration->getRenderCheck()) {
28 | case 'usersettings':
29 | return (bool)($beUser->uc['feediting'] ?? false);
30 | case 'adminpanel':
31 | $adminPanelRequestId = $this->getRequest()->getAttribute('adminPanelRequestId');
32 | return is_string($adminPanelRequestId);
33 | }
34 |
35 | return false;
36 | }
37 |
38 | protected function getBackendUser(): ?FrontendBackendUserAuthentication
39 | {
40 | return $GLOBALS['BE_USER'];
41 | }
42 |
43 | protected function getRequest(): ServerRequestInterface
44 | {
45 | return $GLOBALS['TYPO3_REQUEST'];
46 | }
47 | }
--------------------------------------------------------------------------------
/Classes/Edit/Permissions.php:
--------------------------------------------------------------------------------
1 | getBackendUser();
16 | return $user !== null && $user->user['uid'] > 0;
17 | }
18 |
19 | public function editPage(int $pageId): bool
20 | {
21 | $user = $this->getBackendUser();
22 | if (!$user) {
23 | return false;
24 | }
25 | $row = BackendUtility::getRecord('pages', $pageId);
26 | return $user->doesUserHaveAccess($row, Permission::PAGE_EDIT);
27 | }
28 |
29 | public function editElement(string $tableName, array $row): bool
30 | {
31 | $user = $this->getBackendUser();
32 | if (!$user) {
33 | return false;
34 | }
35 | $conf = [
36 | 'allow' => 'edit, new, delete, hide, move, localize, versions, permissions, info, history, workspace, recordInfo',
37 | 'onlyCurrentPid' => false,
38 | ];
39 | return $user->allowedToEdit($tableName, $row, $conf, true);
40 | }
41 |
42 | protected function getBackendUser(): ?BackendUserAuthentication
43 | {
44 | return $GLOBALS['BE_USER'] ?? null;
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # TYPO3 Extension "feediting"
2 |
3 | This extension provides the good old edit pencil icons in the frontend to have a fast experience to edit content.
4 |
5 | - No in-place editing, no hassle with custom frontend implementations
6 | - Link to the known and powerful backend editing
7 | - Works with all content elements, pages and records
8 |
9 | **Be aware:** This extension is heavly under development. Especially the UI is not final and will change.
10 | However, I am more than happy to get feedback and ideas. Please report at https://github.com/georgringer/feediting/issues.
11 |
12 | 
13 |
14 | ## Requirements
15 |
16 | - TYPO3 12 LTS
17 |
18 | ## Installation
19 |
20 | Install this extension via composer `composer require georgringer/feediting`
21 |
22 | ## Usage
23 |
24 | 1. Log in as backend user
25 | 2. Enable/Disable the feature in the user settings
26 |
27 | 
28 |
29 | ### Usage in Extensions
30 |
31 | Use custom ViewHelper, see example below for `EXT:news`
32 |
33 | ```html
34 |
37 |
38 |
39 | {newsItem.title}
40 |
41 |
42 |
43 |
44 | ```
--------------------------------------------------------------------------------
/Classes/Utility/ContentElementOrder.php:
--------------------------------------------------------------------------------
1 | [],
16 | 'next' => [],
17 | 'prevUid' => [],
18 | ];
19 |
20 | public function getList(int $pid, int $colpos, int $language = 0)
21 | {
22 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
23 | $queryResult = $queryBuilder->select('uid', 'header', 'sorting', 'pid')
24 | ->from('tt_content')
25 | ->where(
26 | $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)),
27 | $queryBuilder->expr()->eq('colPos', $queryBuilder->createNamedParameter($colpos, Connection::PARAM_INT)),
28 | $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter($language, Connection::PARAM_INT)),
29 | )
30 | ->orderBy('sorting', 'ASC')
31 | ->executeQuery();
32 |
33 | $prevUid = 0;
34 | $prevPrevUid = 0;
35 | // Get first two rows and initialize prevPrevUid and prevUid if on page > 1
36 | // $row = $queryResult->fetchAssociative();
37 | $row = $queryResult->fetchAssociative();
38 |
39 | $prevPrevUid = ((int)$row['pid']);
40 | $prevUid = $row['uid'];
41 | $backendUser = $GLOBALS['BE_USER'];
42 | // Accumulate rows here
43 | while ($row = $queryResult->fetchAssociative()) {
44 | // In offline workspace, look for alternative record
45 | BackendUtility::workspaceOL('tt_content', $row, $backendUser->workspace, true);
46 | if (is_array($row)) {
47 | if ($prevUid) {
48 | $this->currentTable['prev'][$row['uid']] = $prevPrevUid;
49 | $this->currentTable['next'][$prevUid] = '-' . $row['uid'];
50 | $this->currentTable['prevUid'][$row['uid']] = $prevUid;
51 | }
52 | $prevPrevUid = isset($this->currentTable['prev'][$row['uid']]) ? -$prevUid : $row['pid'];
53 | $prevUid = $row['uid'];
54 | }
55 | }
56 |
57 | return $this->currentTable;
58 | }
59 |
60 | }
--------------------------------------------------------------------------------
/Resources/Public/JavaScript/popover.js:
--------------------------------------------------------------------------------
1 | function isInViewport(element) {
2 | const rect = element.getBoundingClientRect();
3 | const html = document.documentElement;
4 | return rect.top >= 0 &&
5 | rect.left >= 0 &&
6 | rect.bottom <= (window.innerHeight || html.clientHeight) &&
7 | rect.right <= (window.innerWidth || html.clientWidth);
8 | }
9 |
10 | class Popover {
11 | constructor(trigger, { position = 'top', className = 'popover' }) {
12 | this.trigger = trigger;
13 | this.position = top;
14 | this.className = className;
15 | this.orderedPositions = ['top', 'right', 'bottom', 'left'];
16 |
17 | const popoverTemplate = document.querySelector(`[data-popover=${trigger.dataset.popoverTarget}]`);
18 | this.popover = document.createElement('div');
19 | this.popover.innerHTML = popoverTemplate.innerHTML;
20 |
21 | Object.assign(this.popover.style, {
22 | position: 'fixed'
23 | });
24 |
25 | this.popover.classList.add(className);
26 |
27 | this.handleWindowEvent = () => {
28 | if (this.isVisible) {
29 | this.show();
30 | }
31 | };
32 |
33 | this.handleDocumentEvent = (evt) => {
34 | if (this.isVisible && evt.target !== this.trigger && evt.target !== this.popover) {
35 | this.popover.remove();
36 | }
37 | };
38 | }
39 |
40 | get isVisible() {
41 | return document.body.contains(this.popover);
42 | }
43 |
44 | show() {
45 | // document.addEventListener('click', this.handleDocumentEvent);
46 | window.addEventListener('scroll', this.handleWindowEvent);
47 | window.addEventListener('resize', this.handleWindowEvent);
48 |
49 | document.body.appendChild(this.popover);
50 |
51 | const { top: triggerTop, left: triggerLeft } = this.trigger.getBoundingClientRect();
52 | const { offsetHeight: triggerHeight, offsetWidth: triggerWidth } = this.trigger;
53 | const { offsetHeight: popoverHeight, offsetWidth: popoverWidth } = this.popover;
54 |
55 | const positionIndex = this.orderedPositions.indexOf(this.position);
56 |
57 | const positions = {
58 | top: {
59 | name: 'top',
60 | top: triggerTop - popoverHeight,
61 | left: triggerLeft - ((popoverWidth - triggerWidth) / 2)
62 | },
63 | right: {
64 | name: 'right',
65 | top: triggerTop - ((popoverHeight - triggerHeight) / 2),
66 | left: triggerLeft + triggerWidth
67 | },
68 | bottom: {
69 | name: 'bottom',
70 | top: triggerTop + triggerHeight,
71 | left: triggerLeft - ((popoverWidth - triggerWidth) / 2)
72 | },
73 | left: {
74 | name: 'left',
75 | top: triggerTop - ((popoverHeight - triggerHeight) / 2),
76 | left: triggerLeft - popoverWidth
77 | }
78 | };
79 |
80 | const position = this.orderedPositions
81 | .slice(positionIndex)
82 | .concat(this.orderedPositions.slice(0, positionIndex))
83 | .map(pos => positions[pos])
84 | .find(pos => {
85 | this.popover.style.top = `${pos.top}px`;
86 | this.popover.style.left = `${pos.left}px`;
87 | return isInViewport(this.popover);
88 | });
89 |
90 | this.orderedPositions.forEach(pos => {
91 | this.popover.classList.remove(`${this.className}--${pos}`);
92 | });
93 |
94 | if (position) {
95 | this.popover.classList.add(`${this.className}--${position.name}`);
96 | } else {
97 | this.popover.style.top = positions.bottom.top;
98 | this.popover.style.left = positions.bottom.left;
99 | this.popover.classList.add(`${this.className}--bottom`);
100 | }
101 | }
102 |
103 | destroy() {
104 | this.popover.remove();
105 |
106 | document.removeEventListener('click', this.handleDocumentEvent);
107 | window.removeEventListener('scroll', this.handleWindowEvent);
108 | window.removeEventListener('resize', this.handleWindowEvent);
109 | }
110 |
111 | toggle() {
112 | if (this.isVisible) {
113 | this.destroy();
114 | } else {
115 | this.show();
116 | }
117 | }
118 | }
119 |
120 | // DEMO
121 |
122 |
123 | const triggers = document.getElementsByClassName('feediting-popover-trigger');
124 | // foreach triggers
125 | Array.from(triggers).forEach(trigger => {
126 |
127 | let popover = new Popover(trigger, { position: 'bottom' });
128 |
129 |
130 |
131 | trigger.addEventListener('click', () => popover.toggle());
132 | });
133 |
134 |
--------------------------------------------------------------------------------
/Classes/Edit/EditPanel.php:
--------------------------------------------------------------------------------
1 | getBackendUser()) {
32 | return;
33 | }
34 | $accessService = GeneralUtility::makeInstance(AccessService::class);
35 | if (!$accessService->enabled()) {
36 | return;
37 | }
38 |
39 | $this->enabled = true;
40 | $this->eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
41 | $this->permissions = GeneralUtility::makeInstance(Permissions::class);
42 | $pageRow = $this->request->getAttribute('frontend.controller')->page;
43 | // $moduleName = BackendUtility::getPagesTSconfig($row['pid'])['mod.']['newContentElementWizard.']['override'] ?? 'new_content_element_wizard';
44 | // $perms = $this->getBackendUser()->calcPerms($tsfe->page);
45 | $this->permissionsOfPage = $this->getBackendUser()->calcPerms($pageRow);
46 | }
47 |
48 | public function render(string $content): string
49 | {
50 | if (!$this->enabled) {
51 | return '';
52 | }
53 | $isPageTable = $this->tableName === 'pages';
54 | $allowed = $isPageTable ? $this->permissions->editPage($this->row['pid']) : $this->permissions->editElement($this->tableName, $this->row);
55 | if (!$allowed) {
56 | return '';
57 | }
58 |
59 | return $this->renderPanel($content);
60 | }
61 |
62 | protected function collectActions(): array
63 | {
64 | $data = [];
65 |
66 | $event = $this->eventDispatcher->dispatch(
67 | new EditPanelActionEvent(
68 | $this->request,
69 | $this->permissionsOfPage,
70 | $this->tableName,
71 | $this->recordId,
72 | $this->row, $data),
73 |
74 | );
75 | return $event->getActions();
76 | }
77 |
78 | protected function renderPanel(string $content): string
79 | {
80 | $data = $this->collectActions();
81 | if (empty($data)) {
82 | return '';
83 | }
84 |
85 | $assetCollector = GeneralUtility::makeInstance(AssetCollector::class);
86 | $assetCollector->addStylesheet('feediting', 'EXT:feediting/Resources/Public/Styles/basic.css');
87 | $assetCollector->addJavaScript('feediting', 'EXT:feediting/Resources/Public/JavaScript/popover.js');
88 |
89 | array_walk($data, static function (string &$value) {
90 | $value = '' . $value . '';
91 | });
92 |
93 | $infos = [
94 | BackendUtility::getRecordTitle($this->tableName, $this->row),
95 | ];
96 | if ($this->tableName === 'tt_content') {
97 | $infos[] = BackendUtility::getProcessedValue($this->tableName, 'CType', $this->row['CType']);
98 | }
99 |
100 | $identifier = 'trigger' . md5(json_encode($data));
101 | $elementInformation = '' . htmlspecialchars(implode(LF, $infos)) . '[' . $this->recordId . ']
';
102 | $panel = '
103 |
104 |
107 |
108 |
109 | '
110 | . $elementInformation
111 | . '
' . implode(LF, $data) . '
'
112 | . '
113 |
114 |
';
115 | return '' . $content . $panel . '
';
116 | }
117 |
118 | protected function getBackendUser(): ?FrontendBackendUserAuthentication
119 | {
120 | return $GLOBALS['BE_USER'];
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/Resources/Public/Styles/basic.css:
--------------------------------------------------------------------------------
1 |
2 | .tx-feediting-fluidtemplate {
3 | transition: opacity 200ms;
4 | /*opacity: 1;*/
5 | position: relative;
6 | }
7 |
8 | .tx-feediting-fluidtemplate .popover-container{
9 | position: absolute;
10 | top:-50px;
11 | left:50%;
12 | transform: translateX(-50%);
13 | }
14 |
15 | /*body:hover:not(.tx-feediting-fluidtemplate):hover .tx-feediting-fluidtemplate {*/
16 | /* opacity: 0.5;*/
17 | /*}*/
18 |
19 | /*.tx-feediting-fluidtemplate:hover {*/
20 | /* opacity: 1 !important;*/
21 | /*}*/
22 |
23 | .tx-feediting-fluidtemplate-pages:hover {
24 | outline: none;
25 | box-shadow: none;
26 | }
27 |
28 | .tx-feediting-panel {
29 | color: #333 !important;
30 | background-color: rgb(255, 135, 0);
31 | border-radius: 4px;
32 | }
33 |
34 | .tx-feediting-panel a {
35 | text-decoration: none !important;
36 | display: initial !important;
37 | position: initial !important;
38 | }
39 |
40 | .tx-feediting-panel .tx-feediting-type {
41 | font-weight: bold;
42 | padding: 10px;
43 | color:white;
44 | font-size: 20px;
45 | text-align: center;
46 | }
47 |
48 | .tx-feediting-panel .tx-feediting-type span {
49 | font-style: italic;
50 | }
51 |
52 | .tx-feediting-panel .tx-feediting-actions {
53 | padding: 10px;
54 | display:flex;
55 | gap:20px;
56 | flex-direction: row;
57 | justify-content: center;
58 | flex-wrap: wrap;
59 | }
60 |
61 | .tx-feediting-element {
62 | background-color:white;
63 | border-radius: 4px;
64 | }
65 |
66 | .tx-feediting-element a:link,.tx-feediting-element a:visited {
67 | color: inherit;
68 | padding:10px;
69 | display:block !important;
70 | font-size: 14px;
71 | }
72 |
73 | .tx-feediting-element a:hover,.tx-feediting-element a:active, .tx-feediting-element a:focus {
74 | color:rgb(255, 135, 0);
75 | }
76 |
77 | .tx-feediting-element:hover {
78 |
79 | }
80 |
81 | .icon-size-small svg {
82 | display: inline-block;
83 | height: 16px;
84 | line-height: 16px;
85 | width: 16px;
86 | }
87 |
88 |
89 | .tx-feediting-dropdown {
90 | position: relative;
91 | display: inline-block;
92 | }
93 |
94 | .tx-feediting-dropdown > input[type="checkbox"] {
95 | position: absolute;
96 | left: -100vw;
97 | }
98 |
99 | .tx-feediting-dropdown > label,
100 | .tx-feediting-dropdown > a[role="button"] {
101 | display: inline-block;
102 | padding: 6px 15px;
103 | color: #333;
104 | line-height: 1.5em;
105 | text-decoration: none;
106 | border: 1px solid #8c8c8c;
107 | cursor: pointer;
108 | -webkit-border-radius: 3px;
109 | -moz-border-radius: 3px;
110 | border-radius: 3px;
111 | }
112 |
113 | .tx-feediting-dropdown > label:hover,
114 | .tx-feediting-dropdown > a[role="button"]:hover,
115 | .tx-feediting-dropdown > a[role="button"]:focus {
116 | border-color: #333;
117 | }
118 |
119 | .tx-feediting-dropdown > label:after,
120 | .tx-feediting-dropdown > a[role="button"]:after {
121 | content: "x";
122 | display: inline-block;
123 | margin-left: 6px;
124 | }
125 |
126 | .tx-feediting-dropdown > ul {
127 | position: absolute;
128 | z-index: 999;
129 | display: block;
130 | left: -100vw;
131 | top: calc(1.5em + 14px);
132 | border: 1px solid #8c8c8c;
133 | background: #fff;
134 | padding: 6px 0;
135 | margin: 0;
136 | list-style: none;
137 | width: 100%;
138 | -webkit-border-radius: 3px;
139 | -moz-border-radius: 3px;
140 | border-radius: 3px;
141 | -webkit-box-shadow: 0 3px 8px rgba(0, 0, 0, .15);
142 | -moz-box-shadow: 0 3px 8px rgba(0, 0, 0, .15);
143 | box-shadow: 0 3px 8px rgba(0, 0, 0, .15);
144 | }
145 |
146 | .tx-feediting-dropdown > ul a {
147 | display: block;
148 | padding: 6px 15px;
149 | text-decoration: none;
150 | color: #333;
151 | }
152 |
153 | .tx-feediting-dropdown > ul a:hover,
154 | .tx-feediting-dropdown > ul a:focus {
155 | background: #ececec;
156 | }
157 |
158 | .tx-feediting-dropdown > input[type="checkbox"]:checked ~ ul,
159 | .tx-feediting-dropdown > ul:target {
160 | left: 0;
161 | }
162 |
163 | .tx-feediting-dropdown > [type="checkbox"]:checked + label:after,
164 | .tx-feediting-dropdown > ul:target ~ a:after {
165 | content: "\f0d8";
166 | }
167 |
168 | .tx-feediting-dropdown a.close {
169 | display: none;
170 | }
171 |
172 | .tx-feediting-dropdown > ul:target ~ a.close {
173 | display: block;
174 | position: absolute;
175 | left: 0;
176 | top: 0;
177 | height: 100%;
178 | width: 100%;
179 | text-indent: -100vw;
180 | z-index: 1000;
181 | }
182 |
183 |
184 | /*@keyframes slide-top {*/
185 | /* 0% {*/
186 | /* opacity: 0;*/
187 | /* transform: translateY(-15%)*/
188 | /* }*/
189 | /* 100% {*/
190 | /* opacity: 1;*/
191 | /* transform: translateY(0)*/
192 | /* }*/
193 | /*}*/
194 |
195 | @keyframes slide-right {
196 | 0% {
197 | opacity: 0;
198 | transform: translateX(15%)
199 | }
200 | 100% {
201 | opacity: 1;
202 | transform: translateX(0)
203 | }
204 | }
205 |
206 | @keyframes slide-bottom {
207 | 0% {
208 | opacity: 0;
209 | transform: translateY(15%)
210 | }
211 | 100% {
212 | opacity: 1;
213 | transform: translateY(0)
214 | }
215 | }
216 |
217 | @keyframes slide-left {
218 | 0% {
219 | opacity: 0;
220 | transform: translateX(-15%)
221 | }
222 | 100% {
223 | opacity: 1;
224 | transform: translateX(0)
225 | }
226 | }
227 |
228 | .popover {
229 | border-radius: 4px;
230 | background: #fff;
231 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
232 | min-width: 90vw;
233 | z-index:9999
234 | }
235 |
236 | .popover--top {
237 | margin-top: -16px;
238 | animation: .4s slide-top
239 | }
240 |
241 | .popover--top::before, .popover--top::after {
242 | content: "";
243 | position: absolute;
244 | top: 100%;
245 | left: 50%;
246 | margin-left: -8px;
247 | border: 8px solid transparent;
248 | border-top-color: #fff
249 | }
250 |
251 | .popover--top::before {
252 | margin-top: 1px;
253 | border-top-color: #6a6a6a
254 | }
255 |
256 | .popover--right {
257 | margin-left: 16px;
258 | animation: .4s slide-right
259 | }
260 |
261 | .popover--right::before, .popover--right::after {
262 | content: "";
263 | position: absolute;
264 | top: 50%;
265 | right: 100%;
266 | margin-top: -8px;
267 | border: 8px solid transparent;
268 | border-right-color: #fff
269 | }
270 |
271 | .popover--right::before {
272 | margin-right: 1px;
273 | border-right-color: #6a6a6a
274 | }
275 |
276 | .popover--bottom {
277 | margin-top: 16px;
278 | animation: .4s slide-bottom
279 | }
280 |
281 | .popover--bottom::before, .popover--bottom::after {
282 | content: "";
283 | position: absolute;
284 | bottom: 100%;
285 | left: 50%;
286 | margin-left: -8px;
287 | border: 8px solid transparent;
288 | border-bottom-color: rgb(255, 135, 0);
289 | }
290 |
291 | .popover--bottom::before {
292 | margin-bottom: 1px;
293 | border-bottom-color: rgb(255, 135, 0);
294 | }
295 |
296 | .popover--left {
297 | margin-left: -16px;
298 | animation: .4s slide-left
299 | }
300 |
301 | .popover--left::before, .popover--left::after {
302 | content: "";
303 | position: absolute;
304 | top: 50%;
305 | left: 100%;
306 | margin-top: -8px;
307 | border: 8px solid transparent;
308 | border-left-color: #fff
309 | }
310 |
311 | .popover--left::before {
312 | margin-left: 1px;
313 | border-left-color: #6a6a6a
314 | }
315 |
316 | .feediting-popover-trigger {
317 | display: block;
318 | position: relative;
319 | cursor: pointer;
320 | margin: 0 auto;
321 |
322 | appearance: none;
323 | background-color: white;
324 | border:4px solid rgb(255, 135, 0);
325 | border-radius: 100%;
326 | width:50px;
327 | height:50px;
328 | }
329 |
330 | .pages .feediting-popover-trigger {
331 | border: 4px solid #0080c9;
332 | }
333 |
334 | .pages.tx-feediting-panel {
335 | background-color: #0080c9;
336 | }
337 |
338 | .popover-container.pages {
339 | top: auto !important;
340 | bottom: 10px;
341 | transform: none;
342 | z-index: 9999;
343 | }
344 |
--------------------------------------------------------------------------------
/Classes/EventListener/DefaultEditPanelActionEventListener.php:
--------------------------------------------------------------------------------
1 | uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
35 | $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
36 | }
37 |
38 | public function __invoke(EditPanelActionEvent $event)
39 | {
40 | $this->tableName = $event->table;
41 | $this->recordId = $event->id;
42 | $this->row = $event->row;
43 | $this->permissionsOfPage = $event->permissionsOfPage;
44 | $this->tsfe = $event->request->getAttribute('frontend.controller');
45 | $actions = $event->getActions();
46 |
47 | $this->edit($actions);
48 | $this->move($actions);
49 | if ($this->tableName === 'tt_content') {
50 | $this->moveUpDown($actions);
51 | }
52 | if ($this->tableName === 'pages') {
53 | $this->newPage($actions);
54 | $this->linkToListView($actions);
55 | $this->newContent($actions);
56 | } elseif ($this->tableName !== 'tt_content') {
57 | $this->newRecord($actions);
58 | }
59 | $this->history($actions);
60 | $this->info($actions);
61 |
62 |
63 | $event->setActions($actions);
64 | }
65 |
66 | protected function edit(array &$data)
67 | {
68 | $link = $this->uriBuilder->buildUriFromRoute(
69 | 'record_edit',
70 | [
71 | 'edit[' . $this->tableName . '][' . $this->recordId . ']' => 'edit',
72 | 'noView' => 1,
73 | 'returnUrl' => $this->getReturnUrl(),
74 | ]
75 | );
76 | $data[] = $this->generateLink($link, 'actions-page-open', 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.edit');
77 | }
78 |
79 | protected function move(array &$data): void
80 | {
81 | if ($this->tableName === 'pages') {
82 | $link = $this->uriBuilder->buildUriFromRoute(
83 | 'move_page',
84 | [
85 | 'uid' => $this->recordId,
86 | 'returnUrl' => $this->getReturnUrl(),
87 | ]
88 | );
89 | } else {
90 | $link = $this->uriBuilder->buildUriFromRoute(
91 | 'move_element',
92 | [
93 | 'table' => $this->tableName,
94 | 'uid' => $this->recordId,
95 | 'returnUrl' => $this->getReturnUrl(),
96 | ]
97 | );
98 | }
99 | $data[] = $this->generateLink($link, 'actions-document-move', 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:move', true);
100 | }
101 |
102 | protected function moveUpDown(array &$data): void
103 | {
104 | $order = GeneralUtility::makeInstance(ContentElementOrder::class);
105 | $list = $order->getList($this->row['pid'], $this->row['colPos'], $this->row['sys_language_uid']);
106 | foreach (['up', 'down'] as $direction) {
107 | $checkKey = $direction === 'up' ? 'prev' : 'next';
108 | if (!isset($list[$checkKey][$this->row['uid']])) {
109 | continue;
110 | }
111 | $params = [];
112 | $params['redirect'] = $this->getReturnUrl();
113 | $params['returnUrl'] = $this->getReturnUrl();
114 | $params['cmd'][$this->tableName][$this->recordId]['move'] = $list[$checkKey][$this->row['uid']];
115 | $url = $this->uriBuilder->buildUriFromRoute('tce_db', $params);
116 |
117 | $data[] = $this->generateLink($url, 'actions-move-' . $direction, 'LLL:EXT:core/Resources/Private/Language/locallang_tsfe.xlf:p_move' . ucfirst($direction));
118 | }
119 | }
120 |
121 | protected function history(array &$data)
122 | {
123 | // History
124 | $link = $this->uriBuilder->buildUriFromRoute(
125 | 'record_history',
126 | [
127 | 'element' => $this->tableName . ':' . $this->recordId,
128 | 'returnUrl' => $this->getReturnUrl(),
129 | ]
130 | );
131 | $data[] = $this->generateLink($link, 'actions-document-history-open', 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.history', true);
132 | }
133 |
134 | protected function info(array &$data): void
135 | {
136 | $link = $this->uriBuilder->buildUriFromRoute(
137 | 'show_item',
138 | [
139 | 'table' => $this->tableName,
140 | 'uid' => $this->recordId,
141 | 'returnUrl' => $this->getReturnUrl(),
142 | ]
143 | );
144 | $data[] = $this->generateLink($link, 'actions-document-info', 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info');
145 | }
146 |
147 | protected function newPage(array &$data): void
148 | {
149 | if ($this->permissionsOfPage & Permission::PAGE_NEW) {
150 | $link = $this->uriBuilder->buildUriFromRoute(
151 | 'db_new',
152 | [
153 | 'id' => $this->recordId,
154 | 'pagesOnly' => 1,
155 | 'returnUrl' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'),
156 | ]
157 | );
158 | $data[] = $this->generateLink($link, 'actions-page-new', 'LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:newPage2');
159 | }
160 | }
161 |
162 | protected function newContent(array &$data)
163 | {
164 | if (!($this->permissionsOfPage & Permission::CONTENT_EDIT)) {
165 | return;
166 | }
167 | $backendLayoutview = GeneralUtility::makeInstance(BackendLayoutView::class);
168 | $layout = $backendLayoutview->getSelectedBackendLayout($this->tsfe->page['uid']);
169 | $items = [];
170 | if ($layout && !empty($layout['__items'])) {
171 | $items = $layout['__items'];
172 | }
173 |
174 | if (empty($items)) {
175 | return;
176 | }
177 | // todo set ctype, ...?
178 | // todo which language?
179 |
180 | $defVals = [];
181 | $links = [];
182 | $identifier = 'dropdown-' . md5($this->tableName . $this->recordId);
183 | foreach ($items as $item) {
184 | $link = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
185 | 'edit' => [
186 | 'tt_content' => [
187 | $this->tsfe->page['uid'] => 'new',
188 | ],
189 | ],
190 | 'returnUrl' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'),
191 | 'defVals' => [
192 | 'tt_content' => array_replace($defVals, [
193 | 'colPos' => $item['value'],
194 | 'sys_language_uid' => $this->tsfe->page['sys_language_uid'],
195 | ]),
196 | ],
197 | ]);
198 | $links[] = sprintf('%s', htmlspecialchars($link), $item['label']);
199 | }
200 | if (!empty($links)) {
201 | $data[] = '
202 |
203 |
207 |
208 | ' . implode(LF, $links) . '
209 |
210 |
';
211 | }
212 | }
213 |
214 | protected function newRecord(array &$data)
215 | {
216 | $targetPageRow = BackendUtility::getRecord('pages', $this->row['pid']);
217 | $permissionsOfTargetPage = $this->getBackendUser()->calcPerms($targetPageRow);
218 | if (!($permissionsOfTargetPage & Permission::CONTENT_EDIT)) {
219 | return;
220 | }
221 | $link = $this->uriBuilder->buildUriFromRoute('record_edit', [
222 | 'edit' => [
223 | $this->tableName => [
224 | $targetPageRow['uid'] => 'new',
225 | ],
226 | ],
227 | 'returnUrl' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'),
228 | 'defVals' => [],
229 | ]);
230 | $tableTitle = $this->getLanguageService()->sL($GLOBALS['TCA'][$this->tableName]['ctrl']['title']) ?: $GLOBALS['TCA'][$this->tableName]['ctrl']['title'] ?: $this->tableName;
231 | $targetPageTitle = BackendUtility::getRecordTitle('pages', $targetPageRow);
232 | $label = sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createNewRecord'), $tableTitle, $targetPageTitle);
233 | $data[] = $this->generateLink($link, 'actions-add', $label);
234 | }
235 |
236 | protected function linkToListView(array &$data)
237 | {
238 | // Open list view
239 | if ($this->getBackendUser()->check('modules', 'web_list')) {
240 | $link = $this->uriBuilder->buildUriFromRoute(
241 | 'web_list',
242 | [
243 | 'id' => $this->recordId,
244 | 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI'),
245 | ]
246 | );
247 | $data[] = $this->generateLink($link, 'actions-system-list-open', 'LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:goToListModule');
248 | }
249 | }
250 |
251 |
252 | protected function clipboard(array &$data)
253 | {
254 | $isSel = 'xxx';
255 | $copyUrl = $this->clipboard->selUrlDB($this->tableName, $this->recordId, true, true);
256 | $icon = $isSel === 'copy' ? 'actions-edit-copy-release' : 'actions-edit-copy';
257 | $data[] = '
258 |
259 | ' . $icon . '
260 | ';
261 | }
262 |
263 |
264 | protected function generateLink(UriInterface $link, string $iconIdentifier, string $linkLabel, bool $lightbox = false): string
265 | {
266 | $icon = $this->iconFactory->getIcon($iconIdentifier, Icon::SIZE_SMALL)->render();
267 | $label = $this->getLanguageService()->sL($linkLabel) ?: $linkLabel;
268 | $label = sprintf('%s %s', $icon, htmlspecialchars($label));
269 | $classList = [];
270 | if ($lightbox) {
271 | $classList[] = 'tx-feediting-lightbox';
272 | }
273 | return '' . $label . '';
274 | }
275 |
276 | protected function getLanguageService(): LanguageService
277 | {
278 | return $GLOBALS['LANG'];
279 | }
280 |
281 | protected function getBackendUser(): ?FrontendBackendUserAuthentication
282 | {
283 | return $GLOBALS['BE_USER'];
284 | }
285 |
286 | protected function getReturnUrl(): string
287 | {
288 | return GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL') . '#tx-feediting' . $this->recordId;
289 | }
290 |
291 | }
--------------------------------------------------------------------------------