Skip to content
Snippets Groups Projects
RichTextElement.php 50.9 KiB
Newer Older
<?php
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 SGalinski\Tinymce4Rte\Extension\Typo3Image;
use SGalinski\Tinymce4Rte\Extension\Typo3Link;
use SGalinski\Tinymce4Rte\RteHtmlAreaApi;
use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\DatabaseConnection;
use TYPO3\CMS\Core\FrontendEditing\FrontendEditingController;
use TYPO3\CMS\Core\Html\RteHtmlParser;
use TYPO3\CMS\Core\Localization\Locales;
use TYPO3\CMS\Core\Localization\LocalizationFactory;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\ClientUtility;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Lang\LanguageService;

/**
 * Render rich text editor in FormEngine
 */
class RichTextElement extends AbstractFormElement {
	/**
	 * Main result array as defined in initializeResultArray() of AbstractNode
	 *
	 * @var array
	 */
	protected $resultArray;

	/**
	 * pid of page record the TSconfig is located at.
	 * This is pid of record if table is not pages, or uid if table is pages
	 *
	 * @var int
	 */
	protected $pidOfPageRecord;

	/**
	 * pid of fixed versioned record.
	 * This is the pid of the record in normal cases, but is changed to the pid
	 * of the "mother" record in case the handled record is a versioned overlay
	 * and "mother" is located at a different pid.
	 *
	 * @var int
	 */
	protected $pidOfVersionedMotherRecord;

	/**
	 * Native, not further processed TsConfig of RTE section for this record on given pid.
	 *
	 * Example:
	 *
	 * RTE = foo
	 * RTE.bar = xy
	 *
	 * array(
	 *    'value' => 'foo',
	 *    'properties' => array(
	 *        'bar' => 'xy',
	 *    ),
	 * );
	 *
	 * @var array
	 */
	protected $vanillaRteTsConfig;

	/**
	 * Based on $vanillaRteTsConfig, this property contains "processed" configuration
	 * where table and type specific RTE setup is merged into 'default.' array.
	 *
	 * @var array
	 */
	protected $processedRteConfiguration;

	/**
	 * An unique identifier based on field name to have id attributes in HTML referenced in javascript.
	 *
	 * @var string
	 */
	protected $domIdentifier;

	/**
	 * Parsed "defaultExtras" TCA
	 *
	 * @var array
	 */
	protected $defaultExtras;

	/**
	 * Some client info containing "user agent", "browser", "version", "system"
	 *
	 * @var array
	 */
	protected $client;

	/**
	 * Selected language
	 *
	 * @var string
	 */
	protected $language;

	/**
	 * TYPO3 language code of the content language
	 *
	 * @var string
	 */
	protected $contentTypo3Language;

	/**
	 * ISO language code of the content language
	 *
	 * @var string
	 */
	protected $contentISOLanguage;

	/**
	 * Uid of chosen content language
	 *
	 * @var int
	 */
	protected $contentLanguageUid;

	/**
	 * The order of the toolbar: the name is the TYPO3-button name
	 *
	 * @var string
	 */
	protected $defaultToolbarOrder;

	/**
	 * Conversion array: TYPO3 button names to htmlArea button names
	 *
	 * @var array
	 */
	protected $convertToolbarForHtmlAreaArray = array(
		'space' => 'space',
		'bar' => 'separator',
		'linebreak' => 'linebreak'
	);

	/**
	 * Final toolbar array
	 *
	 * @var array
	 */
	protected $toolbar = array();

	/**
	 * Save the buttons for the toolbar
	 *
	 * @var array
	 */
	protected $toolbarOrderArray = array();

	/**
	 * Plugin buttons
	 *
	 * @var array
	 */
	protected $pluginButton = array();

	/**
	 * Plugin labels
	 *
	 * @var array
	 */
	protected $pluginLabel = array();

	/**
	 * Array of plugin id's enabled in the current RTE editing area
	 * @var array
	 */
	protected $pluginEnabledArray = array();

	/**
	 * Cumulative array of plugin id's enabled so far in any of the RTE editing areas of the form
	 *
	 * @var array
	 */
	protected $pluginEnabledCumulativeArray = array();

	/**
	 * Array of registered plugins indexed by their plugin Id's
	 *
	 * @var array
	protected $registeredPlugins = array();

	/**
	 * This will render a <textarea> OR RTE area form field,
	 * possibly with various control/validation features
	 *
	 * @return array As defined in initializeResultArray() of AbstractNode
	 */
	public function render() {
		$table = $this->data['tableName'];
		$fieldName = $this->data['fieldName'];
		$row = $this->data['databaseRow'];
		$parameterArray = $this->data['parameterArray'];

		$backendUser = $this->getBackendUserAuthentication();

		$this->resultArray = $this->initializeResultArray();
		$this->defaultExtras = BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras']);
		$this->pidOfPageRecord = $this->data['effectivePid'];
		BackendUtility::fixVersioningPid($table, $row);
		$this->pidOfVersionedMotherRecord = (int) $row['pid'];
		$this->vanillaRteTsConfig = $backendUser->getTSConfig('RTE', BackendUtility::getPagesTSconfig($this->pidOfPageRecord));
		$this->processedRteConfiguration = BackendUtility::RTEsetup(
			$this->vanillaRteTsConfig['properties'],
			$table,
			$fieldName,
			$this->data['recordTypeValue']
		$this->client = $this->clientInfo();
		$this->domIdentifier = preg_replace('/[^a-zA-Z0-9_:.-]/', '_', $parameterArray['itemFormElName']);
		$this->domIdentifier = htmlspecialchars(preg_replace('/^[^a-zA-Z]/', 'x', $this->domIdentifier));
//
//		$this->initializeLanguageRelatedProperties();
//
		// Get skin file name from Page TSConfig if any
//		$skinFilename = trim($this->processedRteConfiguration['skin']) ?: 'EXT:rtehtmlarea/Resources/Public/Css/Skin/htmlarea.css';
//		$skinFilename = $this->getFullFileName($skinFilename);
//		$skinDirectory = dirname($skinFilename);
//
//		// jQuery UI Resizable style sheet and main skin stylesheet
//		$this->resultArray['stylesheetFiles'][] = $skinDirectory . '/jquery-ui-resizable.css';
//		$this->resultArray['stylesheetFiles'][] = $skinFilename;
        $this->enableRegisteredPlugins();

//        // Configure toolbar
//        $this->setToolbar();
//
//        // Check if some plugins need to be disabled
//        $this->setPlugins();
//
        // Merge the list of enabled plugins with the lists from the previous RTE editing areas on the same form
        $this->pluginEnabledCumulativeArray = $this->pluginEnabledArray;

//        $this->addOnSubmitJavaScriptCode();

        // Add RTE JavaScript

//        // Create language labels
//        $this->createJavaScriptLanguageLabelsFromFiles();

        // Get RTE init JS code
        $this->resultArray['additionalJavaScriptPost'][] = $this->getRteInitJsCode();
		$this->addInstanceJavaScriptRegistration();

		$html = $this->getMainHtml();

		$this->resultArray['html'] = $this->renderWizards(
			array($html),
			$parameterArray['fieldConf']['config']['wizards'],
			$table,
			$row,
			$fieldName,
			$parameterArray,
			$parameterArray['itemFormElName'],
			$this->defaultExtras,
			TRUE
		);

		return $this->resultArray;
	}

	/**
	 * Create main HTML elements
	 *
	 * @return string Main RTE html
	 */
	protected function getMainHtml() {
		$backendUser = $this->getBackendUserAuthentication();

		if ($this->isInFullScreenMode()) {
			$height = '100%';
			$paddingRight = '0px';
			$editorWrapWidth = '100%';
		} else {
			$options = $backendUser->userTS['options.'];
			/** @var InlineStackProcessor $inlineStackProcessor */
			$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
			$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
			$height = 380 + (isset($options['RTELargeHeightIncrement']) ? (int) $options['RTELargeHeightIncrement'] : 0);
			$heightOverride = isset($backendUser->uc['rteHeight']) && (int) $backendUser->uc['rteHeight'] ?: (int) $this->processedRteConfiguration['RTEHeightOverride'];
			$height = $heightOverride > 0 ? $heightOverride . 'px' : $height . 'px';
			$paddingRight = '2';
			$editorWrapWidth = '99%';
		}
		$rteDivStyle = 'position:relative; left:0px; top:0px; height:' . $height . '; width: 100%;' . '; border: 1px solid black; padding: 2 ' . $paddingRight . ' 2 2;';

		$itemFormElementName = $this->data['parameterArray']['itemFormElName'];

		// This seems to result in:
		//	_TRANSFORM_bodytext (the handled field name) in case the field is a direct DB field
		//	_TRANSFORM_vDEF (constant string) in case the RTE is within a flex form
		$triggerFieldName = preg_replace('/\\[([^]]+)\\]$/', '[_TRANSFORM_\\1]', $itemFormElementName);

		$value = $this->transformDatabaseContentToEditor($this->data['parameterArray']['itemFormElValue']);

		// Remove this empty data attribute, otherwise an error will throw in TYPO3 8.7.
		$validationResults = $this->getValidationDataAsDataAttribute(
			$this->data['parameterArray']['fieldConf']['config']
		);
		if (VersionUtility::isVersion870OrHigher() &&
			trim($validationResults) === 'data-formengine-validation-rules="[]"'
		) {
			$validationResults = '';
		}

		$result = array();
		// next line is required to fix the scrolling toolbar elements in the tinymce
		// also fixes the fullscreen dialog
		$result[] = '<style>body{position:relative;}div.mce-fullscreen{top: 65px;}</style>';
		// The hidden field tells the DataHandler that processing should be done on this value.
		$result[] = '<input type="hidden" name="' . htmlspecialchars($triggerFieldName) . '" value="RTE" />';
//		$result[] = '<div id="pleasewait' . $this->domIdentifier . '" class="pleasewait" style="display: block;" >';
//		$result[] =    $this->getLanguageService()->sL('LLL:EXT:tinymce4_rte/Resources/Private/Language/locallang.xlf:Please wait');
//		$result[] = '</div>';
		$result[] = '<div id="editorWrap' . $this->domIdentifier . '" class="editorWrap" style="width:' . $editorWrapWidth . '; height:100%;">';
		$result[] = '<textarea ' . $validationResults . ' id="RTEarea' . $this->domIdentifier . '" class="tinymce4_rte" name="' . htmlspecialchars($itemFormElementName) . '" rows="0" cols="0" style="' . htmlspecialchars($rteDivStyle) . '">';
		$result[] = htmlspecialchars($value);
		$result[] = '</textarea>';
		$result[] = '</div>';

		return implode(LF, $result);
	}

	/**
	 * Add registered plugins to the array of enabled plugins
	 *
	 * @return void
	 */
	protected function enableRegisteredPlugins() {
			'TYPO3Image' => [
				'objectReference' => Typo3Image::class,
			],
			'TYPO3Link' => [
				'objectReference' => TYPO3Link::class,
			],
		];
		foreach ($plugins as $pluginId => $pluginObjectConfiguration) {
			if (is_array($pluginObjectConfiguration) && isset($pluginObjectConfiguration['objectReference'])) {
				/** @var RteHtmlAreaApi $plugin */
				$plugin = GeneralUtility::makeInstance($pluginObjectConfiguration['objectReference']);
				$configuration = array(
					'language' => $this->language,
					'contentTypo3Language' => $this->contentTypo3Language,
					'contentISOLanguage' => $this->contentISOLanguage,
					'contentLanguageUid' => $this->contentLanguageUid,
					'RTEsetup' => $this->vanillaRteTsConfig,
					'client' => $this->client,
					'thisConfig' => $this->processedRteConfiguration,
					'specConf' => $this->defaultExtras,
				);
				if ($plugin->main($configuration)) {
					$this->registeredPlugins[$pluginId] = $plugin;
					// Override buttons from previously registered plugins
					$pluginButtons = GeneralUtility::trimExplode(',', $plugin->getPluginButtons(), TRUE);
					foreach ($this->pluginButton as $previousPluginId => $buttonList) {
						$this->pluginButton[$previousPluginId] = implode(',', array_diff(GeneralUtility::trimExplode(',', $this->pluginButton[$previousPluginId], TRUE), $pluginButtons));
					}
					$this->pluginButton[$pluginId] = $plugin->getPluginButtons();
					$pluginLabels = GeneralUtility::trimExplode(',', $plugin->getPluginLabels(), TRUE);
					foreach ($this->pluginLabel as $previousPluginId => $labelList) {
						$this->pluginLabel[$previousPluginId] = implode(',', array_diff(GeneralUtility::trimExplode(',', $this->pluginLabel[$previousPluginId], TRUE), $pluginLabels));
					}
					$this->pluginLabel[$pluginId] = $plugin->getPluginLabels();
					$this->pluginEnabledArray[] = $pluginId;
				}
			}
		}

		// Process overrides
		$hidePlugins = array();
		foreach ($this->registeredPlugins as $pluginId => $plugin) {
			/** @var RteHtmlAreaApi $plugin */
			if ($plugin->addsButtons() && !$this->pluginButton[$pluginId]) {
				$hidePlugins[] = $pluginId;
			}
		}
		$this->pluginEnabledArray = array_unique(array_diff($this->pluginEnabledArray, $hidePlugins));
	/**
	 * Set the toolbar config (only in this PHP-Object, not in JS):
	 *
	 * @return void
	 */
	protected function setToolbar() {
		$backendUser = $this->getBackendUserAuthentication();

		if ($this->client['browser'] === 'msie' || $this->client['browser'] === 'opera') {
			$this->processedRteConfiguration['keepButtonGroupTogether'] = 0;
		}
		$this->defaultToolbarOrder = 'bar, blockstylelabel, blockstyle, textstylelabel, textstyle, linebreak,
			bar, formattext, bold,  strong, italic, emphasis, big, small, insertedtext, deletedtext, citation, code,'
			. 'definition, keyboard, monospaced, quotation, sample, variable, bidioverride, strikethrough, subscript, superscript, underline, span,
			bar, fontstyle, fontsize, bar, formatblock, insertparagraphbefore, insertparagraphafter, blockquote, line,
			bar, left, center, right, justifyfull,
			bar, orderedlist, unorderedlist, definitionlist, definitionitem, outdent, indent,
			bar, language, showlanguagemarks,lefttoright, righttoleft,
			bar, textcolor, bgcolor, textindicator,
			bar, editelement, showmicrodata,
			bar, image, emoticon, insertcharacter, insertsofthyphen, abbreviation, user,
			bar, link, unlink,
			bar, table,'
			. ($this->processedRteConfiguration['hideTableOperationsInToolbar']
			&& is_array($this->processedRteConfiguration['buttons.'])
			&& is_array($this->processedRteConfiguration['buttons.']['toggleborders.'])
			&& $this->processedRteConfiguration['buttons.']['toggleborders.']['keepInToolbar'] ? ' toggleborders,' : '')
			. 'bar, findreplace, spellcheck,
			bar, chMode, inserttag, removeformat, bar, copy, cut, paste, pastetoggle, pastebehaviour, bar, undo, redo, bar, about, linebreak,'
			. ($this->processedRteConfiguration['hideTableOperationsInToolbar'] ? '' : 'bar, toggleborders,')
			. ' bar, tableproperties, tablerestyle, bar, rowproperties, rowinsertabove, rowinsertunder, rowdelete, rowsplit, bar,
			columnproperties, columninsertbefore, columninsertafter, columndelete, columnsplit, bar,
			cellproperties, cellinsertbefore, cellinsertafter, celldelete, cellsplit, cellmerge';

		// Additional buttons from registered plugins
		foreach ($this->registeredPlugins as $pluginId => $plugin) {
			/** @var RteHtmlAreaApi $plugin */
			if ($this->isPluginEnabled($pluginId)) {
				$pluginButtons = $plugin->getPluginButtons();
				//Add only buttons not yet in the default toolbar order
				$addButtons = implode(
					',',
					array_diff(
						GeneralUtility::trimExplode(',', $pluginButtons, TRUE),
						GeneralUtility::trimExplode(',', $this->defaultToolbarOrder, TRUE)
					)
				);
				$this->defaultToolbarOrder = ($addButtons ? 'bar,' . $addButtons . ',linebreak,' : '') . $this->defaultToolbarOrder;
			}
		}
		$toolbarOrder = $this->processedRteConfiguration['toolbarOrder'] ?: $this->defaultToolbarOrder;
		// Getting rid of undefined buttons
		$this->toolbarOrderArray = array_intersect(GeneralUtility::trimExplode(',', $toolbarOrder, TRUE), GeneralUtility::trimExplode(',', $this->defaultToolbarOrder, TRUE));
		$toolbarOrder = array_unique(array_values($this->toolbarOrderArray));
		// Fetching specConf for field from backend
		$pList = is_array($this->defaultExtras['richtext']['parameters']) ? implode(',', $this->defaultExtras['richtext']['parameters']) : '';
		if ($pList !== '*') {
			// If not all
			$show = is_array($this->defaultExtras['richtext']['parameters']) ? $this->defaultExtras['richtext']['parameters'] : array();
			if ($this->processedRteConfiguration['showButtons']) {
				if (!GeneralUtility::inList($this->processedRteConfiguration['showButtons'], '*')) {
					$show = array_unique(array_merge($show, GeneralUtility::trimExplode(',', $this->processedRteConfiguration['showButtons'], TRUE)));
				} else {
					$show = array_unique(array_merge($show, $toolbarOrder));
				}
			}
			if (is_array($this->processedRteConfiguration['showButtons.'])) {
				foreach ($this->processedRteConfiguration['showButtons.'] as $buttonId => $value) {
					if ($value) {
						$show[] = $buttonId;
					}
				$show = array_unique($show);
		} else {
			$show = $toolbarOrder;
		}
		$RTEkeyList = isset($backendUser->userTS['options.']['RTEkeyList']) ? $backendUser->userTS['options.']['RTEkeyList'] : '*';
		if ($RTEkeyList !== '*') {
			// If not all
			$show = array_intersect($show, GeneralUtility::trimExplode(',', $RTEkeyList, TRUE));
		}
		// Hiding buttons of disabled plugins
		$hideButtons = array('space', 'bar', 'linebreak');
		foreach ($this->pluginButton as $pluginId => $buttonList) {
			if (!$this->isPluginEnabled($pluginId)) {
				$buttonArray = GeneralUtility::trimExplode(',', $buttonList, TRUE);
				foreach ($buttonArray as $button) {
					$hideButtons[] = $button;
				}
			}
		}
		// Hiding labels of disabled plugins
		foreach ($this->pluginLabel as $pluginId => $label) {
			if (!$this->isPluginEnabled($pluginId)) {
				$hideButtons[] = $label;
			}
		}
		// Hiding buttons
		$show = array_diff($show, GeneralUtility::trimExplode(',', $this->processedRteConfiguration['hideButtons'], TRUE));
		// Apply toolbar constraints from registered plugins
		foreach ($this->registeredPlugins as $pluginId => $plugin) {
			if ($this->isPluginEnabled($pluginId) && method_exists($plugin, 'applyToolbarConstraints')) {
				$show = $plugin->applyToolbarConstraints($show);
			}
		}
		// Getting rid of the buttons for which we have no position
		$show = array_intersect($show, $toolbarOrder);
		foreach ($this->registeredPlugins as $pluginId => $plugin) {
			/** @var RteHtmlAreaApi $plugin */
			$plugin->setToolbar($show);
		}
		$this->toolbar = $show;
	}
	/**
	 * Disable some plugins
	 *
	 * @return void
	 */
	protected function setPlugins() {
		// Disabling a plugin that adds buttons if none of its buttons is in the toolbar
		$hidePlugins = array();
		foreach ($this->pluginButton as $pluginId => $buttonList) {
			/** @var RteHtmlAreaApi $plugin */
			$plugin = $this->registeredPlugins[$pluginId];
			if ($plugin->addsButtons()) {
				$showPlugin = FALSE;
				$buttonArray = GeneralUtility::trimExplode(',', $buttonList, TRUE);
				foreach ($buttonArray as $button) {
					if (in_array($button, $this->toolbar)) {
						$showPlugin = TRUE;
				}
				if (!$showPlugin) {
					$hidePlugins[] = $pluginId;
				}
			}
		}
		$this->pluginEnabledArray = array_diff($this->pluginEnabledArray, $hidePlugins);
		// Hiding labels of disabled plugins
		$hideLabels = array();
		foreach ($this->pluginLabel as $pluginId => $label) {
			if (!$this->isPluginEnabled($pluginId)) {
				$hideLabels[] = $label;
			}
		}
		$this->toolbar = array_diff($this->toolbar, $hideLabels);
		// Adding plugins declared as prerequisites by enabled plugins
		$requiredPlugins = array();
		foreach ($this->registeredPlugins as $pluginId => $plugin) {
			/** @var RteHtmlAreaApi $plugin */
			if ($this->isPluginEnabled($pluginId)) {
				$requiredPlugins = array_merge($requiredPlugins, GeneralUtility::trimExplode(',', $plugin->getRequiredPlugins(), TRUE));
			}
		}
		$requiredPlugins = array_unique($requiredPlugins);
		foreach ($requiredPlugins as $pluginId) {
			if (is_object($this->registeredPlugins[$pluginId]) && !$this->isPluginEnabled($pluginId)) {
				$this->pluginEnabledArray[] = $pluginId;
			}
		}
		$this->pluginEnabledArray = array_unique($this->pluginEnabledArray);
		// Completing the toolbar conversion array for htmlArea
		foreach ($this->registeredPlugins as $pluginId => $plugin) {
			/** @var RteHtmlAreaApi $plugin */
			if ($this->isPluginEnabled($pluginId)) {
				$this->convertToolbarForHtmlAreaArray = array_unique(array_merge($this->convertToolbarForHtmlAreaArray, $plugin->getConvertToolbarForHtmlAreaArray()));
			}
		}
	}

	/**
	 * Add RTE main scripts and plugin scripts
	 *
	 * @return void
	 */
	protected function loadRequireModulesForRTE() {
		/** @var Loader $tinyMCE */
		$tinyMCE = GeneralUtility::makeInstance(Loader::class);
		$tinyMCE->loadConfiguration($this->vanillaRteTsConfig['properties']['default.']['tinymceConfiguration']);

		$contentCssArray = [];
		if (VersionUtility::isVersion870OrHigher()) {
			$contentCssArray = is_array($this->vanillaRteTsConfig['properties']['default.']['contentCSS.']) ?
				$this->vanillaRteTsConfig['properties']['default.']['contentCSS.'] : [];
		} else {
			if ($this->vanillaRteTsConfig['properties']['default.']['contentCSS'] !== '') {
				$contentCssArray = is_array($this->vanillaRteTsConfig['properties']['default.']['contentCSS.']) ?
					$this->vanillaRteTsConfig['properties']['default.']['contentCSS.'] :
					(array) $this->vanillaRteTsConfig['properties']['default.']['contentCSS'];
			}
		}

		if (!empty($contentCssArray)) {
			$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));
			'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', $this->domIdentifier);

		$this->resultArray['requireJsModules'] = $tinyMCE->loadJsViaRequireJS();
	}

	/**
	 * Return RTE initialization inline JavaScript code
	 *
	 * @return string RTE initialization inline JavaScript code
	 */
	protected function getRteInitJsCode() {
		$ajaxPingJavaScriptCode = '(function($) {
			$(document).ready(function() {
				$.get(TYPO3.settings.ajaxUrls[\'tinymce4_rte::ajaxPing\']);
			});
		})(TYPO3.jQuery);';

		return 'if (typeof RTEarea === "undefined") {
			RTEarea = new Object();
			RTEarea[0] = new Object();
			RTEarea[0].version = "' . $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['tinymce4_rte']['version'] . '";
			RTEarea[0].editorUrl = "' . ExtensionManagementUtility::extRelPath('tinymce4_rte') . '";
		}' . $ajaxPingJavaScriptCode;
	}

	/**
	 * Return the Javascript code for configuring the RTE
	 *
	 * @return void
	 */
	protected function addInstanceJavaScriptRegistration() {
		$backendUser = $this->getBackendUserAuthentication();

		$jsArray = array();
//		$jsArray[] = 'if (typeof configureEditorInstance === "undefined") {';
//		$jsArray[] = '	configureEditorInstance = new Object();';
//		$jsArray[] = '}';
//		$jsArray[] = 'configureEditorInstance["' . $this->domIdentifier . '"] = function() {';
//		$jsArray[] = 'if (typeof RTEarea === "undefined" || typeof SG === "undefined") {';
//		$jsArray[] = '	window.setTimeout("configureEditorInstance[' . GeneralUtility::quoteJSvalue($this->domIdentifier) . ']();", 40);';
//		$jsArray[] = '} else {';
		$jsArray[] = 'editornumber = "' . $this->domIdentifier . '";';
		$jsArray[] = 'var RTEarea = window.RTEarea || {};';
		$jsArray[] = 'RTEarea[editornumber] = new Object();';
		$jsArray[] = 'RTEarea[editornumber].RTEtsConfigParams = "&RTEtsConfigParams=' . rawurlencode($this->RTEtsConfigParams()) . '";';
		$jsArray[] = 'RTEarea[editornumber].number = editornumber;';
		$jsArray[] = 'RTEarea[editornumber].deleted = false;';
		$jsArray[] = 'RTEarea[editornumber].textAreaId = "' . $this->domIdentifier . '";';
		$jsArray[] = 'RTEarea[editornumber].id = "RTEarea" + editornumber;';
		$jsArray[] = 'RTEarea[editornumber].RTEWidthOverride = "'
			. (isset($backendUser->uc['rteWidth']) && trim($backendUser->uc['rteWidth'])
				? trim($backendUser->uc['rteWidth'])
				: trim($this->processedRteConfiguration['RTEWidthOverride'])) . '";';
		$jsArray[] = 'RTEarea[editornumber].RTEHeightOverride = "'
			. (isset($backendUser->uc['rteHeight']) && (int) $backendUser->uc['rteHeight']
				? (int) $backendUser->uc['rteHeight']
				: (int) $this->processedRteConfiguration['RTEHeightOverride']) . '";';
		$jsArray[] = 'RTEarea[editornumber].resizable = '
			. (isset($backendUser->uc['rteResize']) && $backendUser->uc['rteResize']
				? 'true;'
				: (trim($this->processedRteConfiguration['rteResize']) ? 'true;' : 'false;'));
		$jsArray[] = 'RTEarea[editornumber].maxHeight = "'
			. (isset($backendUser->uc['rteMaxHeight']) && (int) $backendUser->uc['rteMaxHeight']
				? trim($backendUser->uc['rteMaxHeight'])
				: ((int) $this->processedRteConfiguration['rteMaxHeight'] ?: '2000')) . '";';
		$jsArray[] = 'RTEarea[editornumber].fullScreen = ' . ($this->isInFullScreenMode() ? 'true;' : 'false;');
		$jsArray[] = 'RTEarea[editornumber].showStatusBar = ' . (trim($this->processedRteConfiguration['showStatusBar']) ? 'true;' : 'false;');
		$jsArray[] = 'RTEarea[editornumber].enableWordClean = ' . (trim($this->processedRteConfiguration['enableWordClean']) ? 'true;' : 'false;');
		$jsArray[] = 'RTEarea[editornumber].htmlRemoveComments = ' . (trim($this->processedRteConfiguration['removeComments']) ? 'true;' : 'false;');
		$jsArray[] = 'RTEarea[editornumber].disableEnterParagraphs = ' . (trim($this->processedRteConfiguration['disableEnterParagraphs']) ? 'true;' : 'false;');
		$jsArray[] = 'RTEarea[editornumber].disableObjectResizing = ' . (trim($this->processedRteConfiguration['disableObjectResizing']) ? 'true;' : 'false;');
		$jsArray[] = 'RTEarea[editornumber].removeTrailingBR = ' . (trim($this->processedRteConfiguration['removeTrailingBR']) ? 'true;' : 'false;');
		$jsArray[] = 'RTEarea[editornumber].useCSS = ' . (trim($this->processedRteConfiguration['useCSS']) ? 'true' : 'false') . ';';
		$jsArray[] = 'RTEarea[editornumber].keepButtonGroupTogether = ' . (trim($this->processedRteConfiguration['keepButtonGroupTogether']) ? 'true;' : 'false;');
		$jsArray[] = 'RTEarea[editornumber].disablePCexamples = ' . (trim($this->processedRteConfiguration['disablePCexamples']) ? 'true;' : 'false;');
		$jsArray[] = 'RTEarea[editornumber].showTagFreeClasses = ' . (trim($this->processedRteConfiguration['showTagFreeClasses']) ? 'true;' : 'false;');
		$jsArray[] = 'RTEarea[editornumber].tceformsNested = ' . (!empty($this->data) ? json_encode($this->data['tabAndInlineStack']) : '[]') . ';';
		$jsArray[] = 'RTEarea[editornumber].dialogueWindows = new Object();';
		if (isset($this->processedRteConfiguration['dialogueWindows.']['defaultPositionFromTop'])) {
			$jsArray[] = 'RTEarea[editornumber].dialogueWindows.positionFromTop = ' . (int) $this->processedRteConfiguration['dialogueWindows.']['defaultPositionFromTop'] . ';';
		}
		if (isset($this->processedRteConfiguration['dialogueWindows.']['defaultPositionFromLeft'])) {
			$jsArray[] = 'RTEarea[editornumber].dialogueWindows.positionFromLeft = ' . (int) $this->processedRteConfiguration['dialogueWindows.']['defaultPositionFromLeft'] . ';';
		}
		$jsArray[] = 'RTEarea[editornumber].sys_language_content = "' . $this->contentLanguageUid . '";';
		$jsArray[] = 'RTEarea[editornumber].typo3ContentLanguage = "' . $this->contentTypo3Language . '";';
		$jsArray[] = 'RTEarea[editornumber].userUid = "' . 'BE_' . $backendUser->user['uid'] . '";';

		// Setting the plugin flags
		$jsArray[] = 'RTEarea[editornumber].plugin = new Object();';
		foreach ($this->pluginEnabledArray as $pluginId) {
			$jsArray[] = 'RTEarea[editornumber].plugin.' . $pluginId . ' = true;';
		}

		// Setting the buttons configuration
		$jsArray[] = 'RTEarea[editornumber].buttons = new Object();';
		if (is_array($this->processedRteConfiguration['buttons.'])) {
			foreach ($this->processedRteConfiguration['buttons.'] as $buttonIndex => $conf) {
				$button = substr($buttonIndex, 0, -1);
				if (is_array($conf)) {
					$jsArray[] = 'RTEarea[editornumber].buttons.' . $button . ' = ' . $this->buildNestedJSArray($conf) . ';';
				}
			}
		}

		// Setting the list of tags to be removed if specified in the RTE config
		if (trim($this->processedRteConfiguration['removeTags'])) {
			$jsArray[] = 'RTEarea[editornumber].htmlRemoveTags = /^(' . implode('|', GeneralUtility::trimExplode(',', $this->processedRteConfiguration['removeTags'], TRUE)) . ')$/i;';
		}

		// Setting the list of tags to be removed with their contents if specified in the RTE config
		if (trim($this->processedRteConfiguration['removeTagsAndContents'])) {
			$jsArray[] = 'RTEarea[editornumber].htmlRemoveTagsAndContents = /^(' . implode('|', GeneralUtility::trimExplode(',', $this->processedRteConfiguration['removeTagsAndContents'], TRUE)) . ')$/i;';
		}

		// Setting array of custom tags if specified in the RTE config
		if (!empty($this->processedRteConfiguration['customTags'])) {
			$customTags = GeneralUtility::trimExplode(',', $this->processedRteConfiguration['customTags'], TRUE);
			if (!empty($customTags)) {
				$jsArray[] = 'RTEarea[editornumber].customTags= ' . json_encode($customTags) . ';';
			}
		}

		// Setting array of content css files if specified in the RTE config
		$versionNumberedFileNames = array();
//		$contentCssFileNames = $this->getContentCssFileNames();
//		foreach ($contentCssFileNames as $contentCssFileName) {
//			$versionNumberedFileNames[] = GeneralUtility::createVersionNumberedFilename($contentCssFileName);
//		}
		$jsArray[] = 'RTEarea[editornumber].pageStyle = ["' . implode('","', $versionNumberedFileNames) . '"];';

		$jsArray[] = 'RTEarea[editornumber].classesUrl = "' . $this->writeTemporaryFile(('classes_' . $this->language), 'js', $this->buildJSClassesArray()) . '";';

		// Add Javascript configuration for registered plugins
		foreach ($this->registeredPlugins as $pluginId => $plugin) {
			/** @var RteHtmlAreaApi $plugin */
			if ($this->isPluginEnabled($pluginId)) {
				$jsPluginString = $plugin->buildJavascriptConfiguration();
				if ($jsPluginString) {
					$jsArray[] = $plugin->buildJavascriptConfiguration();
				}
			}
		}

		// Avoid premature reference to HTMLArea when being initially loaded by IRRE Ajax call
		$jsArray[] = 'RTEarea[editornumber].toolbar = ' . $this->getJSToolbarArray() . ';';
		$jsArray[] = 'RTEarea[editornumber].convertButtonId = ' . json_encode(array_flip($this->convertToolbarForHtmlAreaArray)) . ';';
		$jsArray[] = 'RTEarea[editornumber].editor = {
			getPlugin: function(pluginId) {
				return plugin;
			}
		};';
		//$jsArray[] = 'RTEarea.initEditor(editornumber);';
//		$jsArray[] = '}';
//		$jsArray[] = '}';
//		$jsArray[] = 'configureEditorInstance["' . $this->domIdentifier . '"]();';

		$this->resultArray['additionalJavaScriptPost'][] = implode(LF, $jsArray);
	}

	/**
	 * Get the name of the contentCSS files to use
	 *
	 * @return array An array of full file name of the content css files to use
	 */
	protected function getContentCssFileNames() {
		$contentCss = is_array($this->processedRteConfiguration['contentCSS.']) ? $this->processedRteConfiguration['contentCSS.'] : array();
		if (isset($this->processedRteConfiguration['contentCSS'])) {
			$contentCss[] = trim($this->processedRteConfiguration['contentCSS']);
		}
		$contentCssFiles = array();
		if (!empty($contentCss)) {
			foreach ($contentCss as $contentCssKey => $contentCssfile) {
				$fileName = trim($contentCssfile);
				$absolutePath = GeneralUtility::getFileAbsFileName($fileName);
				if (file_exists($absolutePath) && filesize($absolutePath)) {
					$contentCssFiles[$contentCssKey] = $this->getFullFileName($fileName);
				}
			}
		} else {
			// Fallback to default content css file if none of the configured files exists and is not empty
			$contentCssFiles['default'] = $this->getFullFileName('EXT:rtehtmlarea/Resources/Public/Css/ContentCss/Default.css');
		}
		return array_unique($contentCssFiles);
	}

	/**
	 * Return TRUE, if the plugin can be loaded
	 *
	 * @param string $pluginId : The identification string of the plugin
	 * @return bool TRUE if the plugin can be loaded
	 */
	protected function isPluginEnabled($pluginId) {
		return in_array($pluginId, $this->pluginEnabledArray);
	}

	/**
	 * Return JS arrays of classes configuration
	 *
	 * @return string JS classes arrays
	 */
	protected function buildJSClassesArray() {
		$RTEProperties = $this->vanillaRteTsConfig['properties'];
		// Declare sub-arrays
		$classesArray = array(
			'labels' => array(),
			'values' => array(),
			'noShow' => array(),
			'alternating' => array(),
			'counting' => array(),
			'selectable' => array(),
			'requires' => array(),
			'requiredBy' => array(),
			'XOR' => array()
		);
		$JSClassesArray = '';
		// Scanning the list of classes if specified in the RTE config
		if (is_array($RTEProperties['classes.'])) {
			foreach ($RTEProperties['classes.'] as $className => $conf) {
				$className = rtrim($className, '.');

				$label = '';
				if (!empty($conf['name'])) {
					$label = $this->getLanguageService()->sL(trim($conf['name']));
					$label = str_replace('"', '\\"', str_replace('\\\'', '\'', $label));
				}
				$classesArray['labels'][$className] = $label;
				$classesArray['values'][$className] = str_replace('\\\'', '\'', $conf['value']);
				if (isset($conf['noShow'])) {
					$classesArray['noShow'][$className] = $conf['noShow'];
				}
				if (is_array($conf['alternating.'])) {
					$classesArray['alternating'][$className] = $conf['alternating.'];
				}
				if (is_array($conf['counting.'])) {
					$classesArray['counting'][$className] = $conf['counting.'];
				}
				if (isset($conf['selectable'])) {
					$classesArray['selectable'][$className] = $conf['selectable'];
				}
				if (isset($conf['requires'])) {
					$classesArray['requires'][$className] = explode(',', GeneralUtility::rmFromList($className, $this->cleanList($conf['requires'])));
				}
			}
			// Remove circularities from classes dependencies
			$requiringClasses = array_keys($classesArray['requires']);
			foreach ($requiringClasses as $requiringClass) {
				if ($this->hasCircularDependency($classesArray, $requiringClass, $requiringClass)) {
					unset($classesArray['requires'][$requiringClass]);
				}
			}
			// Reverse relationship for the dependency checks when removing styles
			$requiringClasses = array_keys($classesArray['requires']);
			foreach ($requiringClasses as $className) {
				foreach ($classesArray['requires'][$className] as $requiredClass) {
					if (!is_array($classesArray['requiredBy'][$requiredClass])) {
						$classesArray['requiredBy'][$requiredClass] = array();
					}
					if (!in_array($className, $classesArray['requiredBy'][$requiredClass])) {
						$classesArray['requiredBy'][$requiredClass][] = $className;
					}
				}
			}
		}
		// Scanning the list of sets of mutually exclusives classes if specified in the RTE config
		if (is_array($RTEProperties['mutuallyExclusiveClasses.'])) {
			foreach ($RTEProperties['mutuallyExclusiveClasses.'] as $listName => $conf) {
				$classSet = GeneralUtility::trimExplode(',', $conf, TRUE);
				$classList = implode(',', $classSet);
				foreach ($classSet as $className) {
					$classesArray['XOR'][$className] = '/^(' . implode('|', GeneralUtility::trimExplode(',', GeneralUtility::rmFromList($className, $classList), TRUE)) . ')$/';
				}
			}
		}
		foreach ($classesArray as $key => $subArray) {
			$JSClassesArray .= 'HTMLArea.classes' . ucfirst($key) . ' = ' . $this->buildNestedJSArray($subArray) . ';' . LF;
		}
		return $JSClassesArray;
	}

	/**
	 * Check for possible circularity in classes dependencies
	 *
	 * @param array $classesArray : reference to the array of classes dependencies
	 * @param string $requiringClass : class requiring at some iteration level from the initial requiring class
	 * @param string $initialClass : initial class from which a circular relationship is being searched
	 * @param int $recursionLevel : depth of recursive call
	 * @return bool TRUE, if a circular relationship is found
	 */
	protected function hasCircularDependency(&$classesArray, $requiringClass, $initialClass, $recursionLevel = 0) {
		if (is_array($classesArray['requires'][$requiringClass])) {
			if (in_array($initialClass, $classesArray['requires'][$requiringClass])) {
				return TRUE;
			} else {
				if ($recursionLevel++ < 20) {
					foreach ($classesArray['requires'][$requiringClass] as $requiringClass2) {
						if ($this->hasCircularDependency($classesArray, $requiringClass2, $initialClass, $recursionLevel)) {
							return TRUE;
						}
					}
				}
				return FALSE;
			}
		} else {
			return FALSE;
	/**
	 * Translate Page TS Config array in JS nested array definition
	 * Replace 0 values with false
	 * Unquote regular expression values
	 * Replace empty arrays with empty objects
	 *
	 * @param array $conf : Page TSConfig configuration array
	 * @return string nested JS array definition
	 */
	protected function buildNestedJSArray($conf) {
		$convertedConf = GeneralUtility::removeDotsFromTS($conf);
		return str_replace(
			array(':"0"', ':"\\/^(', ')$\\/i"', ':"\\/^(', ')$\\/"', '[]'),
			array(':false', ':/^(', ')$/i', ':/^(', ')$/', '{}'), json_encode($convertedConf)
	/**
	 * Writes contents in a file in typo3temp and returns the file name
	 *
	 * @param string $label : A label to insert at the beginning of the name of the file
	 * @param string $fileExtension : The file extension of the file, defaulting to 'js'
	 * @param string $contents : The contents to write into the file
	 * @return string The name of the file written to typo3temp
	 * @throws \RuntimeException If writing to file failed
	 */
	protected function writeTemporaryFile($label, $fileExtension = 'js', $contents = '') {
		$relativeFilename = 'typo3temp/Tinymce4Rte/' . str_replace('-', '_', $label) . '_' . GeneralUtility::shortMD5($contents, 20) . '.' . $fileExtension;
		$destination = PATH_site . $relativeFilename;
		if (!file_exists($destination)) {
			$minifiedJavaScript = '';
			if ($fileExtension === 'js' && $contents !== '') {
				$minifiedJavaScript = GeneralUtility::minifyJavaScript($contents);
			}
			$failure = GeneralUtility::writeFileToTypo3tempDir($destination, $minifiedJavaScript ? $minifiedJavaScript : $contents);
			if ($failure) {
				throw new \RuntimeException($failure, 1294585668);
			}
		}
		if (isset($GLOBALS['TSFE'])) {
			$fileName = $relativeFilename;
		} else {
			$fileName = '../' . $relativeFilename;
		}
		return GeneralUtility::resolveBackPath($fileName);
	}

	/**
	 * Both rte framework and rte plugins can have label files that are
	 * used in JS. The methods gathers those and creates a JS object from
	 * file labels.
	 *
	 * @return string
	 */
	protected function createJavaScriptLanguageLabelsFromFiles() {
		$labelArray = array();
		// Load labels of 3 base files into JS
		foreach (array('tooltips', 'msg', 'dialogs') as $identifier) {
			$fileName = 'EXT:tinymce4_rte/Resources/Private/Language/locallang_' . $identifier . '.xlf';
			$newLabels = $this->getMergedLabelsFromFile($fileName);
			if (!empty($newLabels)) {
				$labelArray[$identifier] = $newLabels;
			}
		}
		// Load labels of plugins into JS