From f3b3342fa4c9de56b5678101d9f44ec92d4e5bf3 Mon Sep 17 00:00:00 2001 From: Fabian Galinski <fabian@sgalinski.de> Date: Thu, 14 Sep 2017 02:44:54 +0200 Subject: [PATCH] [FEATURE] Integration of the tinyMCE for TYPO3 8 --- .../Form/Element/CkEditorRichTextElement.php | 376 ++++++++++++++++++ ...ent.php => RtehtmlareaRichTextElement.php} | 2 +- .../Form/Resolver/RichTextNodeResolver.php | 73 ++-- Classes/Utility/VersionUtility.php | 83 ++++ ext_localconf.php | 8 +- 5 files changed, 512 insertions(+), 30 deletions(-) create mode 100644 Classes/Form/Element/CkEditorRichTextElement.php rename Classes/Form/Element/{RichTextElement.php => RtehtmlareaRichTextElement.php} (99%) create mode 100644 Classes/Utility/VersionUtility.php diff --git a/Classes/Form/Element/CkEditorRichTextElement.php b/Classes/Form/Element/CkEditorRichTextElement.php new file mode 100644 index 0000000..4a2336b --- /dev/null +++ b/Classes/Form/Element/CkEditorRichTextElement.php @@ -0,0 +1,376 @@ +<?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; + } +} diff --git a/Classes/Form/Element/RichTextElement.php b/Classes/Form/Element/RtehtmlareaRichTextElement.php similarity index 99% rename from Classes/Form/Element/RichTextElement.php rename to Classes/Form/Element/RtehtmlareaRichTextElement.php index d3a9ab6..3ceee7f 100644 --- a/Classes/Form/Element/RichTextElement.php +++ b/Classes/Form/Element/RtehtmlareaRichTextElement.php @@ -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 * diff --git a/Classes/Form/Resolver/RichTextNodeResolver.php b/Classes/Form/Resolver/RichTextNodeResolver.php index f1830d8..2501ab1 100644 --- a/Classes/Form/Resolver/RichTextNodeResolver.php +++ b/Classes/Form/Resolver/RichTextNodeResolver.php @@ -1,4 +1,5 @@ <?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; } diff --git a/Classes/Utility/VersionUtility.php b/Classes/Utility/VersionUtility.php new file mode 100644 index 0000000..1ba637f --- /dev/null +++ b/Classes/Utility/VersionUtility.php @@ -0,0 +1,83 @@ +<?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() + ); + } +} diff --git a/ext_localconf.php b/ext_localconf.php index bbb2822..8121163 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -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 -- GitLab