Commit f3b3342f authored by Fabian Galinski's avatar Fabian Galinski 😾
Browse files

[FEATURE] Integration of the tinyMCE for TYPO3 8

parent 06401289
<?php
declare(strict_types=1);
namespace SGalinski\Tinymce4Rte\Form\Element;
/*
* 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 SGalinski\Tinymce\Loader;
use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\Locales;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* Render rich text editor in FormEngine
*/
class CkEditorRichTextElement extends AbstractFormElement {
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This property contains configuration related to the RTE
* But only the .editor configuration part
*
* @var array
*/
protected $rteConfiguration = [];
/**
* Renders the ckeditor element
*
* @return array
* @throws \UnexpectedValueException
* @throws \BadFunctionCallException
* @throws \InvalidArgumentException
*/
public function render(): array {
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$fieldId = $this->sanitizeFieldId($parameterArray['itemFormElName']);
$itemFormElementName = $this->data['parameterArray']['itemFormElName'];
$value = $this->data['parameterArray']['itemFormElValue'] ?? '';
$legacyWizards = $this->renderWizards();
$legacyFieldControlHtml = implode(LF, $legacyWizards['fieldControl']);
$legacyFieldWizardHtml = implode(LF, $legacyWizards['fieldWizard']);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, FALSE);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $legacyFieldControlHtml . $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, FALSE);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $legacyFieldWizardHtml . $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, FALSE);
// $attributes = [
// 'style' => 'display:none',
// 'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
// 'id' => $fieldId,
// 'name' => htmlspecialchars($itemFormElementName),
// ];
// Modified by fgalinski - Start
$attributes = [
'id' => 'RTEarea' . $fieldId,
'name' => htmlspecialchars($itemFormElementName),
'class' => 'tinymce4_rte',
];
// Modified by fgalinski - End
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
$html[] = '<textarea ' . GeneralUtility::implodeAttributes($attributes, TRUE) . '>';
$html[] = htmlspecialchars($value);
$html[] = '</textarea>';
$html[] = '</div>';
$html[] = '<div class="form-wizards-items-aside">';
$html[] = '<div class="btn-group">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
$html[] = '<div class="form-wizards-items-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
$this->rteConfiguration = $config['richtextConfiguration']['editor'];
// Modified by fgalinski - Start
/** @var Loader $tinyMCE */
$tinyMCE = GeneralUtility::makeInstance(Loader::class);
$tinyMCE->loadConfiguration($config['richtextConfiguration']['tinymceConfiguration']);
$contentCssArray = $config['richtextConfiguration']['contentCSS.'];
if (is_array($contentCssArray) && count($contentCssArray) > 0) {
$contentCssFileArray = [];
foreach ($contentCssArray as $contentCssKey => $contentCssFile) {
$contentCssFileAbs = GeneralUtility::getFileAbsFileName(trim($contentCssFile));
if (is_file($contentCssFileAbs)) {
$contentCssFileArray[] = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . PathUtility::stripPathSitePrefix($contentCssFileAbs) . '?' . filemtime($contentCssFileAbs);
}
}
$tinyMCE->addConfigurationOption('content_css', implode(',', $contentCssFileArray));
}
$tinyMCE->addConfigurationOption(
'changeMethod', 'function() {
var TBE_EDITOR = window.TBE_EDITOR || null;
if (TBE_EDITOR && TBE_EDITOR.fieldChanged && typeof TBE_EDITOR.fieldChanged === \'function\') {
TBE_EDITOR.fieldChanged();
}
}'
);
$tinyMCE->addConfigurationOption('editornumber', $fieldId);
// IRRE
$resultArray['requireJsModules'] = $tinyMCE->loadJsViaRequireJS();
// Modified by fgalinski - End
return $resultArray;
}
/**
* @param string $itemFormElementName
*
* @return string
*/
protected function sanitizeFieldId(string $itemFormElementName): string {
$fieldId = preg_replace('/[^a-zA-Z0-9_:.-]/', '_', $itemFormElementName);
return htmlspecialchars(preg_replace('/^[^a-zA-Z]/', 'x', $fieldId));
}
/**
* Gets the JavaScript code for CKEditor module
* Compiles the configuration, and then adds plugins
*
* @param string $fieldId
*
* @return string
*/
protected function getCkEditorRequireJsModuleCode(string $fieldId): string {
$configuration = $this->prepareConfigurationForEditor();
$externalPlugins = '';
foreach ($this->getExtraPlugins() as $pluginName => $config) {
$configuration[$pluginName] = $config['config'];
$configuration['extraPlugins'] .= ',' . $pluginName;
$externalPlugins .= 'CKEDITOR.plugins.addExternal(';
$externalPlugins .= GeneralUtility::quoteJSvalue($pluginName) . ',';
$externalPlugins .= GeneralUtility::quoteJSvalue($config['resource']) . ',';
$externalPlugins .= '\'\');';
}
return 'function(CKEDITOR) {
' . $externalPlugins . '
$(function(){
CKEDITOR.replace("' . $fieldId . '", ' . json_encode($configuration) . ');
require([\'jquery\', \'TYPO3/CMS/Backend/FormEngine\'], function($, FormEngine) {
CKEDITOR.instances["' . $fieldId . '"].on(\'change\', function() {
CKEDITOR.instances["' . $fieldId . '"].updateElement();
FormEngine.Validation.validate();
FormEngine.Validation.markFieldAsChanged($(\'#' . $fieldId . '\'));
});
});
});
}';
}
/**
* Compiles the configuration set from the outside
* to have it easily injected into the CKEditor.
*
* @return array the configuration
*/
protected function prepareConfigurationForEditor(): array {
// Ensure custom config is empty so nothing additional is loaded
// Of course this can be overridden by the editor configuration below
$configuration = [
'customConfig' => '',
];
if (is_array($this->rteConfiguration['config'])) {
$configuration = array_replace_recursive($configuration, $this->rteConfiguration['config']);
}
// Set the UI language of the editor if not hard-coded by the existing configuration
if (empty($configuration['language'])) {
$configuration['language'] = $this->getBackendUser()->uc['lang'] ?: ($this->getBackendUser()->user['lang'] ?: 'en');
}
$configuration['contentsLanguage'] = $this->getLanguageIsoCodeOfContent();
// Replace all label references
$configuration = $this->replaceLanguageFileReferences($configuration);
// Replace all paths
$configuration = $this->replaceAbsolutePathsToRelativeResourcesPath($configuration);
// there are some places where we define an array, but it needs to be a list in order to work
if (is_array($configuration['extraPlugins'])) {
$configuration['extraPlugins'] = implode(',', $configuration['extraPlugins']);
}
if (is_array($configuration['removePlugins'])) {
$configuration['removePlugins'] = implode(',', $configuration['removePlugins']);
}
if (is_array($configuration['removeButtons'])) {
$configuration['removeButtons'] = implode(',', $configuration['removeButtons']);
}
return $configuration;
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUser() {
return $GLOBALS['BE_USER'];
}
/**
* Determine the contents language iso code
*
* @return string
*/
protected function getLanguageIsoCodeOfContent(): string {
$currentLanguageUid = $this->data['databaseRow']['sys_language_uid'];
if (is_array($currentLanguageUid)) {
$currentLanguageUid = $currentLanguageUid[0];
}
$contentLanguageUid = (int) max($currentLanguageUid, 0);
if ($contentLanguageUid) {
$contentLanguage = $this->data['systemLanguageRows'][$currentLanguageUid]['iso'];
} else {
$contentLanguage = $this->rteConfiguration['config']['defaultContentLanguage'] ?? 'en_US';
$languageCodeParts = explode('_', $contentLanguage);
$contentLanguage = strtolower($languageCodeParts[0]) . ($languageCodeParts[1] ? '_' . strtoupper($languageCodeParts[1]) : '');
// Find the configured language in the list of localization locales
$locales = GeneralUtility::makeInstance(Locales::class);
// If not found, default to 'en'
if (!in_array($contentLanguage, $locales->getLocales(), TRUE)) {
$contentLanguage = 'en';
}
}
return $contentLanguage;
}
/**
* Add configuration to replace LLL: references with the translated value
*
* @param array $configuration
*
* @return array
*/
protected function replaceLanguageFileReferences(array $configuration): array {
foreach ($configuration as $key => $value) {
if (is_array($value)) {
$configuration[$key] = $this->replaceLanguageFileReferences($value);
} elseif (is_string($value) && stripos($value, 'LLL:') === 0) {
$configuration[$key] = $this->getLanguageService()->sL($value);
}
}
return $configuration;
}
/**
* Add configuration to replace absolute EXT: paths with relative ones
*
* @param array $configuration
*
* @return array
*/
protected function replaceAbsolutePathsToRelativeResourcesPath(array $configuration): array {
foreach ($configuration as $key => $value) {
if (is_array($value)) {
$configuration[$key] = $this->replaceAbsolutePathsToRelativeResourcesPath($value);
} elseif (is_string($value) && stripos($value, 'EXT:') === 0) {
$configuration[$key] = $this->resolveUrlPath($value);
}
}
return $configuration;
}
/**
* Resolves an EXT: syntax file to an absolute web URL
*
* @param string $value
*
* @return string
*/
protected function resolveUrlPath(string $value): string {
$value = GeneralUtility::getFileAbsFileName($value);
return PathUtility::getAbsoluteWebPath($value);
}
/**
* Get configuration of external/additional plugins
*
* @return array
*/
protected function getExtraPlugins(): array {
$urlParameters = [
'P' => [
'table' => $this->data['tableName'],
'uid' => $this->data['databaseRow']['uid'],
'fieldName' => $this->data['fieldName'],
'recordType' => $this->data['recordTypeValue'],
'pid' => $this->data['effectivePid'],
]
];
$pluginConfiguration = [];
if (isset($this->rteConfiguration['externalPlugins']) && is_array($this->rteConfiguration['externalPlugins'])) {
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
foreach ($this->rteConfiguration['externalPlugins'] as $pluginName => $configuration) {
$pluginConfiguration[$pluginName] = [
'resource' => $this->resolveUrlPath($configuration['resource'])
];
unset($configuration['resource']);
if ($configuration['route']) {
$configuration['routeUrl'] = (string) $uriBuilder->buildUriFromRoute($configuration['route'], $urlParameters);
}
$pluginConfiguration[$pluginName]['config'] = $configuration;
}
}
return $pluginConfiguration;
}
}
......@@ -39,7 +39,7 @@ use TYPO3\CMS\Lang\LanguageService;
/**
* Render rich text editor in FormEngine
*/
class RichTextElement extends AbstractFormElement {
class RtehtmlareaRichTextElement extends AbstractFormElement {
/**
* Main result array as defined in initializeResultArray() of AbstractNode
*
......
<?php
namespace SGalinski\Tinymce4Rte\Form\Resolver;
/*
......@@ -14,7 +15,9 @@ namespace SGalinski\Tinymce4Rte\Form\Resolver;
* The TYPO3 project - inspiring people to share!
*/
use SGalinski\Tinymce4Rte\Form\Element\RichTextElement;
use SGalinski\Tinymce4Rte\Form\Element\CkEditorRichTextElement;
use SGalinski\Tinymce4Rte\Form\Element\RtehtmlareaRichTextElement;
use SGalinski\Tinymce4Rte\Utility\VersionUtility;
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Form\NodeResolverInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
......@@ -44,7 +47,7 @@ class RichTextNodeResolver implements NodeResolverInterface {
/**
* Returns RichTextElement as class name if RTE widget should be rendered.
*
* @return string|void New class name or void if this resolver does not change current class name.
* @return string|NULL New class name or void if this resolver does not change current class name.
*/
public function resolve() {
$table = $this->data['tableName'];
......@@ -52,32 +55,54 @@ class RichTextNodeResolver implements NodeResolverInterface {
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$backendUser = $this->getBackendUserAuthentication();
if (// This field is not read only
!$parameterArray['fieldConf']['config']['readOnly']
// If RTE is generally enabled by user settings and RTE object registry can return something valid
&& $backendUser->isRTE()
) {
// @todo: Most of this stuff is prepared by data providers within $this->data already
$specialConfiguration = BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras']);
// If "richtext" is within defaultExtras
if (isset($specialConfiguration['richtext'])) {
// Operates by reference on $row! 'pid' is changed ...
BackendUtility::fixVersioningPid($table, $row);
list($recordPid, $tsConfigPid) = BackendUtility::getTSCpidCached($table, $row['uid'], $row['pid']);
// If the pid-value is not negative (that is, a pid could NOT be fetched)
if ($tsConfigPid >= 0) {
// Fetch page ts config and do some magic with it to find out if RTE is disabled on TS level.
$rteSetup = $backendUser->getTSConfig('RTE', BackendUtility::getPagesTSconfig($recordPid));
$rteTcaTypeValue = $this->data['recordTypeValue'];
$rteSetupConfiguration = BackendUtility::RTEsetup($rteSetup['properties'], $table, $fieldName, $rteTcaTypeValue);
if (!$rteSetupConfiguration['disabled']) {
// Finally, we're sure the editor should really be rendered ...
return RichtextElement::class;
if (VersionUtility::isVersion870OrHigher()) {
$parameterArray = $this->data['parameterArray'];
$backendUser = $this->getBackendUserAuthentication();
if (// This field is not read only
!$parameterArray['fieldConf']['config']['readOnly']
// If RTE is generally enabled by user settings and RTE object registry can return something valid
&& $backendUser->isRTE()
// If RTE is enabled for field
&& isset($parameterArray['fieldConf']['config']['enableRichtext'])
&& (bool) $parameterArray['fieldConf']['config']['enableRichtext'] === TRUE
// If RTE config is found (prepared by TcaText data provider)
&& isset($parameterArray['fieldConf']['config']['richtextConfiguration'])
&& is_array($parameterArray['fieldConf']['config']['richtextConfiguration'])
// If RTE is not disabled on configuration level
&& !$parameterArray['fieldConf']['config']['richtextConfiguration']['disabled']
) {
return CkEditorRichTextElement::class;
}
} else {
if (// This field is not read only
!$parameterArray['fieldConf']['config']['readOnly']
// If RTE is generally enabled by user settings and RTE object registry can return something valid
&& $backendUser->isRTE()
) {
// @todo: Most of this stuff is prepared by data providers within $this->data already
$specialConfiguration = BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras']);
// If "richtext" is within defaultExtras
if (isset($specialConfiguration['richtext'])) {
// Operates by reference on $row! 'pid' is changed ...
BackendUtility::fixVersioningPid($table, $row);
list($recordPid, $tsConfigPid) = BackendUtility::getTSCpidCached($table, $row['uid'], $row['pid']);
// If the pid-value is not negative (that is, a pid could NOT be fetched)
if ($tsConfigPid >= 0) {
// Fetch page ts config and do some magic with it to find out if RTE is disabled on TS level.
$rteSetup = $backendUser->getTSConfig('RTE', BackendUtility::getPagesTSconfig($recordPid));
$rteTcaTypeValue = $this->data['recordTypeValue'];
$rteSetupConfiguration = BackendUtility::RTEsetup(
$rteSetup['properties'], $table, $fieldName, $rteTcaTypeValue
);
if (!$rteSetupConfiguration['disabled']) {
// Finally, we're sure the editor should really be rendered ...
return RtehtmlareaRichTextElement::class;
}
}
}
}
}
return NULL;
}
......
<?php
namespace SGalinski\Tinymce4Rte\Utility;
/***************************************************************
* Copyright notice
*
* (c) sgalinski Internet Services (https://www.sgalinski.de)
*
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
/**
* Helper class to detect the used TYPO3 version.
*/
class VersionUtility {
/**
* Returns true if the current version ts TYPO3 6.2.
*
* @return bool
*/
public static function isVersion62() {
$versionNumber = self::getVersion();
return ($versionNumber >= 6002000 && $versionNumber < 7000000);
}
/**
* Returns true if the current version ts TYPO3 7.6 and less version 8
*
* @return bool
*/
public static function isVersion76() {
$versionNumber = self::getVersion();
return ($versionNumber >= 7006000 && $versionNumber < 8000000);
}
/**
* Returns true if the current version ts TYPO3 7.6 or later
*
* @return bool
*/
public static function isVersion76OOrHigher() {
return (self::getVersion() >= 7006000);
}
/**
* Returns true if the current version ts TYPO3 8.7 or later
*
* @return bool
*/
public static function isVersion870OrHigher() {
return (self::getVersion() >= 8007000);
}
/**
* Returns the current version as an integer.
*
* @return int
*/
protected static function getVersion() {
return VersionNumberUtility::convertVersionNumberToInteger(
VersionNumberUtility::getNumericTypo3Version()
);
}
}
......@@ -10,11 +10,11 @@ if (!$GLOBALS['TYPO3_CONF_VARS']['BE']['RTEenabled']) {
}
// Register FormEngine node type resolver hook to render RTE in FormEngine if enabled
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeResolver'][1442500255] = array(
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeResolver'][1442500255] = [
'nodeName' => 'text',
'priority' => 40,
'priority' => 60,
'class' => \SGalinski\Tinymce4Rte\Form\Resolver\RichTextNodeResolver::class,
);
];
// load default PageTS config
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig(
......@@ -37,5 +37,3 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeResolver'][1442500255] = a
// Registering soft reference parser for img tags in RTE content
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['softRefParser']['rtehtmlarea_images'] =
\SGalinski\Tinymce4Rte\Hook\SoftReferenceHook::class;
?>
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment