Skip to content

Instantly share code, notes, and snippets.

@joekolade
Last active March 3, 2023 11:09
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save joekolade/674ecba5c2615901581d6c4e4c272b4a to your computer and use it in GitHub Desktop.
[TYPO3 FE Edit] Enable frontend_editing with gridelements in TYPO3 CMS 8.7 #typo3 #typoscript

This work is in progress.

Tested with actual TYPO3 CMS 8.7.8, frontend_editing 1.2.4 and gridelements 8.0.0-dev (from git)

Add the files to an Extension of your choice (e.g. page template extension).

Speaking of frontend_editing 1.2.4 two files inside EXT:frontend_editing must be patched to achieve drag'n'drop out of gridelements.

# Add these constants somewhere
plugin.tx_frontend_editing {
settings {
dropzoneDefaultParams {
tx_gridelements_container = 0
tx_gridelements_columns = 0
}
}
}
<?php
declare(strict_types=1);
namespace Vendor\Extension\Hooks;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\FrontendEditing\EditingPanel\FrontendEditingDropzoneModifier;
use TYPO3\CMS\FrontendEditing\Service\ContentEditableWrapperService;
/**
* Class DropzoneModifier
* @package Vendor\Extension\Hooks
*/
class DropzoneModifier implements \TYPO3\CMS\FrontendEditing\EditingPanel\FrontendEditingDropzoneModifier
{
/**
* Keep list of grid container that has first dropzone
*
* @var string
*/
protected static $containersWithContent = '';
/**
* @param string $table
* @param int $editUid
* @param array $dataArr
* @param string $content
* @return bool
*/
public function wrapWithDropzone(
string $table,
int $editUid,
array $dataArr,
string &$content
): bool {
// CE in gridelement
if ($dataArr['tx_gridelements_container']) {
/** @var ContentEditableWrapperService $wrapperService */
$wrapperService = GeneralUtility::makeInstance(ContentEditableWrapperService::class);
$params = [
'tx_gridelements_container' => $dataArr['tx_gridelements_container'],
'tx_gridelements_columns' => $dataArr['tx_gridelements_columns']
];
$content = $wrapperService->wrapContentWithDropzone(
$table,
(int)$editUid,
$content,
-1
);
$containerIdentifier = $dataArr['tx_gridelements_container'] . '-' . $dataArr['tx_gridelements_columns'];
if (!GeneralUtility::inList(self::$containersWithContent, $containerIdentifier)) {
$content = $wrapperService->wrapContentWithDropzone(
$table,
0,
$content,
-1,
$params,
true
);
self::$containersWithContent .= ',' . $containerIdentifier;
}
$content = str_replace(
[
'ondragstart="window.parent.F.dragCeStart(event)"',
'###GRID_DATA###'
],
[
'ondragstart="window.parent.F.dragCeInsideGridStart(event)"',
sprintf('data-params="%s"', GeneralUtility::implodeArrayForUrl('', $params))
],
$content
);
return true;
}
// Gridelement parent
if ($dataArr['CType'] == 'gridelements_pi1') {
/** @var ContentEditableWrapperService $wrapperService */
$wrapperService = GeneralUtility::makeInstance(ContentEditableWrapperService::class);
// Find empty columns
$columns = $dataArr['tx_gridelements_view_columns'];
foreach ($columns as $key => $column) {
if($dataArr['tx_gridelements_view_column_' . $key] == ''){
$params = [
'tx_gridelements_container' => $dataArr['uid'],
'tx_gridelements_columns' => $key
];
$dropzoneOnly = $wrapperService->wrapContentWithDropzone(
$table,
0,
'',
-1,
$params,
true
);
$dropzoneOnly = str_replace(
[
'ondragstart="window.parent.F.dragCeStart(event)"',
'###GRID_DATA###'
],
[
'ondragstart="window.parent.F.dragCeInsideGridStart(event)"',
sprintf('data-params="%s"', GeneralUtility::implodeArrayForUrl('', $params))
],
$dropzoneOnly
);
$content = str_replace(
[
'<!--###DATA_EMPTY_GRID_DROPZONE_' . $key . '###-->'
],
[
$dropzoneOnly
],
$content
);
}
}
}
return false;
}
}
<!-- Eymaple for Fluid and gridelements -->
<!-- in each column the comment has to be inserted with the corresponding colPos - only if no CEs are there -->
<f:if condition="!{data.tx_gridelements_view_column_21}">
<f:cObject typoscriptObjectPath="lib.emptyColDropzone" data="21"></f:cObject>
</f:if>
<f:format.raw>{data.tx_gridelements_view_column_21->f:format.raw()}</f:format.raw></div>
[globalVar = TSFE : beUserLogin > 0]
page.includeJS {
feeditext = EXT:extension_key/Resources/Public/JavaScripts/FeeditExtend.js
}
lib.gridelements.defaultGridSetup {
stdWrap {
editIcons = tt_content:header, tx_gridelements_children
}
}
lib.emptyColDropzone = TEXT
lib.emptyColDropzone {
current = 1
wrap = <!--###DATA_EMPTY_GRID_DROPZONE_|###-->
}
config.sourceopt {
removeGenerator = {$sourceopt.removeGenerator}
removeBlurScript = {$sourceopt.removeBlurScript}
removeComments = {$sourceopt.removeComments}
removeComments.keep {
3001 = /###DATA_EMPTY_GRID_DROPZONE_/usi
}
}
[global]
console.log('FeeditExtend.js');
(function(w, $) {
var F = w.parent.F;
w.parent.F.dropGridCe = dropGridCe;
w.parent.F.moveRecordInGrid = moveRecordInGrid;
w.parent.F.dragCeInsideGridStart = dragCeInsideGridStart;
// Custom function for grid elements
// Only for move action
function dropGridCe(ev) {
ev.preventDefault();
var movable = parseInt(ev.dataTransfer.getData('movable'), 10);
if (movable === 1) {
var $currentTarget = $(ev.currentTarget);
var ceUid = parseInt(ev.dataTransfer.getData('movableUid'), 10);
var moveAfter = parseInt($currentTarget.data('moveafter'), 10);
var colPos = parseInt($currentTarget.data('colpos'), 10);
if (ceUid !== moveAfter) {
F.moveRecordInGrid(ceUid, 'tt_content', moveAfter, colPos, $currentTarget.data('params'));
}
} else {
F.dropCe(ev);
}
}
function dragCeInsideGridStart(ev) {
ev.stopPropagation();
F.dragCeStart(ev);
}
function moveRecordInGrid(uid, table, beforeUid, colPos, params) {
this.trigger(F.REQUEST_START);
var data = {
uid: uid,
table: table,
beforeUid: beforeUid
};
if (typeof colPos !== 'undefined') {
data.colPos = colPos;
}
$.ajax({
url: F._endpointUrl + '&action=move' + params,
method: 'POST',
data: data
}).done(function (data) {
F.trigger(
F.UPDATE_CONTENT_COMPLETE,
{
message: data.message
}
);
}).fail(function (jqXHR) {
F.trigger(
F.REQUEST_ERROR,
{
message: jqXHR.responseText
}
);
}).always(function () {
F.trigger(F.REQUEST_COMPLETE);
});
}
$('.t3-frontend-editing__ce').on('dragstart', function (event) {
var $currentTarget = $(event.currentTarget);
$currentTarget.addClass('active-drag');
}).on('dragend', function (event) {
var $currentTarget = $(event.currentTarget);
$currentTarget.removeClass('active-drag');
});
})(window, jQuery);
<?php
namespace TYPO3\CMS\FrontendEditing\Hook;
/*
* 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;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\FrontendEditing\Service\AccessService;
use TYPO3\CMS\FrontendEditing\Service\ContentEditableWrapperService;
/**
* Hook is called in ContentObjectRenderer when rendering CONTENT
* It's used to determine if content column is empty and add drop zone
*
* @package TYPO3\CMS\FrontendEditing\Hook
*/
class ContentObjectRendererHook
{
/**
* Render content like in parent object
* If there is not content and table is tt_content - add drop zone
*
* @param string $name
* @param array $conf
* @param string $TSkey
* @param ContentObjectRenderer $pObject
* @return string
*/
public function cObjGetSingleExt(string $name, array $conf, string $TSkey, ContentObjectRenderer $pObject): string
{
$content = '';
$contentObject = $pObject->getContentObject($name);
if ($contentObject) {
$content .= $pObject->render($contentObject, $conf);
// If not content found wrap with drop zone
// Add drop zone only if colPos is set
/** @var AccessService $access */
$access = GeneralUtility::makeInstance(AccessService::class);
if (empty($content)
&& GeneralUtility::_GET('frontend_editing')
&& $access->isEnabled()
&& $conf['table'] === 'tt_content'
&& !empty($conf['select.']['where'])
&& GeneralUtility::isFirstPartOfStr(ltrim($conf['select.']['where']), 'colPos')
) {
list(, $colPos) = GeneralUtility::intExplode('=', $conf['select.']['where'], true);
/** @var ContentEditableWrapperService $wrapperService */
$wrapperService = GeneralUtility::makeInstance(ContentEditableWrapperService::class);
$defaultDropZoneParams = $GLOBALS['TSFE']->tmpl->setup_constants['plugin.']['tx_frontend_editing.']['settings.']['dropzoneDefaultParams.'];
$content = $wrapperService->wrapContentWithDropzone(
$conf['table'],
0,
$content,
$colPos,
$defaultDropZoneParams
);
}
}
return $content;
}
}
<?php
namespace TYPO3\CMS\FrontendEditing\EditingPanel;
/*
* 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;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
use TYPO3\CMS\FrontendEditing\Service\AccessService;
use TYPO3\CMS\FrontendEditing\Service\ContentEditableWrapperService;
/**
* View class for the edit panels in frontend editing
*/
class FrontendEditingPanel
{
/**
* Property for accessing TypoScriptFrontendController centrally
*
* @var TypoScriptFrontendController
*/
protected $frontendController;
/**
* Keep list of columns (colPos) which has content
* so we know if element is first for this column or no
*
* @var string
*/
public static $columnsWithContentList = '';
/**
* Constructor for the edit panel
*/
public function __construct()
{
$this->frontendController = $GLOBALS['TSFE'];
}
/**
* Needs to be implemented via the API but not in use
*
* @param string $content
* @param array $conf
* @param string $currentRecord
* @param array $dataArray
* @param string $table
* @param array $allowedActions
* @param string $newUid
* @param string $fields
* @return string
*/
public function editPanel($content, $conf, $currentRecord, $dataArray, $table, $allowedActions, $newUid, $fields)
{
return $content;
}
/**
* Adds an edit icon to the content string. The edit icon links to EditDocumentController
* with proper parameters for editing the table/fields of the context.
* This implements TYPO3 context sensitive editing facilities.
* Only backend users will have access (if properly configured as well).
* See TYPO3\CMS\Core\FrontendEditing\FrontendEditingController
*
* @param string $content
* @param array $params
* @param array $conf
* @param array $currentRecord
* @param array $dataArr
* @param string $addUrlParamStr
* @param string $table
* @param string $editUid
* @param string $fieldList
* @return string
*/
public function editIcons(
$content,
$params,
array $conf,
$currentRecord,
array $dataArr,
$addUrlParamStr,
$table,
$editUid,
$fieldList
): string {
$access = GeneralUtility::makeInstance(AccessService::class);
if (!$access->isEnabled()) {
return $content;
}
$defaultDropZoneParams = $GLOBALS['TSFE']->tmpl->setup_constants['plugin.']['tx_frontend_editing.']['settings.']['dropzoneDefaultParams.'];
// We need to determine if we are having whole element or just one field for element
// this only allows to edit all other tables just per field instead of per element
$isEditableField = false;
$isWholeElement = false;
if ((int)$conf['beforeLastTag'] === 1) {
$isEditableField = true;
} elseif ($table === 'tt_content' || $conf['hasEditableFields'] === 1) {
$isWholeElement = true;
} else {
// default fallback, for everything else with edit icons, we assume it is separate element and is editable
$isWholeElement = true;
$isEditableField = true;
}
/** @var ContentEditableWrapperService $wrapperService */
$wrapperService = GeneralUtility::makeInstance(ContentEditableWrapperService::class);
if ($isEditableField) {
$fields = GeneralUtility::trimexplode(',', $fieldList);
$content = $wrapperService->wrapContentToBeEditable(
$table,
trim($fields[0]),
(int)$editUid,
$content
);
}
if ($isWholeElement) {
// Special content is about to be shown, so the cache must be disabled.
$this->frontendController->set_no_cache('Display frontend edit icons', true);
// wrap content with controls
$content = $wrapperService->wrapContent(
$table,
(int)$editUid,
$dataArr,
$content
);
$isWrappedWithDropzone = false;
$frontendEditingConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['frontend_editing'];
if (is_array($frontendEditingConfiguration['FrontendEditingPanel']['dropzoneModifiers'])) {
foreach ($frontendEditingConfiguration['FrontendEditingPanel']['dropzoneModifiers'] as $classData) {
$hookObject = GeneralUtility::getUserObj($classData);
if (!$hookObject instanceof FrontendEditingDropzoneModifier) {
throw new \UnexpectedValueException(
$classData . ' must implement interface ' . FrontendEditingDropzoneModifier::class,
1493980015
);
}
$isWrappedWithDropzone = $hookObject->wrapWithDropzone(
$table,
(int)$editUid,
$dataArr,
$content,
NULL,
$defaultDropZoneParams
);
}
}
if (!$isWrappedWithDropzone) {
// @TODO: should there be a config for dropzones like "if ((int)$conf['addDropzone'] > 0)"
// Add a dropzone after content
$content = $wrapperService->wrapContentWithDropzone(
$table,
(int)$editUid,
$content,
(int)$dataArr['colPos'],
$defaultDropZoneParams
);
// If it's first content element for this column wrap with dropzone before content too
if (!GeneralUtility::inList(self::$columnsWithContentList, $dataArr['colPos'])) {
$content = $wrapperService->wrapContentWithDropzone(
$table,
0,
$content,
(int)$dataArr['colPos'],
$defaultDropZoneParams,
true
);
self::$columnsWithContentList .= ',' . $dataArr['colPos'];
}
}
}
return $content;
}
}
@joekolade
Copy link
Author

joekolade commented Jul 10, 2017

DropzoneModifier ist registered via FE-Edit hook (as described in https://docs.typo3.org/typo3cms/extensions/frontend_editing/Development/Hooks/Index.html) :

// Hook for frontend editing $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['frontend_editing']['FrontendEditingPanel']['dropzoneModifiers'][] = \Vendor\Extension\Hooks\DropzoneModifier::class;

@joekolade
Copy link
Author

Two files inside of fronting_editing must be patched tp achieve maximum gridelements integration:

frontend_editing/Classes/EditingPanel/FrontendEditingPanel.php
and
frontend_editing/Classes/Hook/ContentObjectRendererHook.php

Both are part of the gist above.

@ahoffmeyer
Copy link

ahoffmeyer commented Jan 3, 2018

Many thanks for this Gist. Made may day ;)

I created two Xclasses instead of manual patching and it worked for me as well.

If you want to create Xclasses, then you have to copy the FrontendEditingDropzoneModifier interface into the Xclass folder (with proper namespace then), too.

@philippwise
Copy link

The Gist helped me a lot, but I found a small bug.

When adding content in a grid element under another element the tx_gridelements_columns value isn't set properly.

I added $params in DropzoneModifier.php after line 49.

@hirnsturm
Copy link

hirnsturm commented Sep 14, 2018

Many thanks.

My patching solution for FrontendEditingPanel.php and ContentObjectRendererHook.php via composer:

{
    "autoload": {
        "exclude-from-classmap": [
          "web/typo3conf/ext/frontend_editing/Classes/EditingPanel/FrontendEditingPanel.php",
          "web/typo3conf/ext/frontend_editing/Classes/Hook/ContentObjectRendererHook.php"
        ],
        "files": [
          "./patches/frontend_editing/Classes/EditingPanel/FrontendEditingPanel.php",
          "./patches/frontend_editing/Classes/Hook/ContentObjectRendererHook.php"
        ],
        "psr-4": {
          "TYPO3\\CMS\\FrontendEditing\\EditingPanel\\": "./patches/frontend_editing/Classes/EditingPanel/",
          "TYPO3\\CMS\\FrontendEditing\\Hook\\": "./patches/frontend_editing/Classes/Hook/"
        }
    }
}

There is a small bug in your JavaScript. It's throwing errors if Frontend-Editing is disabled. You can fix it by adding the
following if-condition:

if (typeof F === undefined) {
        w.parent.F.dropGridCe = dropGridCe;
        w.parent.F.moveRecordInGrid = moveRecordInGrid;
        w.parent.F.dragCeInsideGridStart = dragCeInsideGridStart;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment