Skip to content

Instantly share code, notes, and snippets.

@joekolade
Last active March 3, 2023 11:09
Show Gist options
  • Save joekolade/674ecba5c2615901581d6c4e4c272b4a to your computer and use it in GitHub Desktop.
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;
}
}
@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