[TASK] Add linkbrowser to CKEditor 01/50901/4
authorFrans Saris <franssaris@gmail.com>
Thu, 8 Dec 2016 15:18:40 +0000 (16:18 +0100)
committerBenni Mack <benni@typo3.org>
Fri, 9 Dec 2016 05:59:15 +0000 (06:59 +0100)
Releases: master
Resolves: #78917
Change-Id: I864b7798cd30f0eec63f4ec719f8dfebca1086ba
Reviewed-on: https://review.typo3.org/50901
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Daniel Gorges <daniel.gorges@b13.de>
Tested-by: Daniel Gorges <daniel.gorges@b13.de>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
typo3/sysext/lang/Resources/Private/Language/locallang_browse_links.xlf
typo3/sysext/rte_ckeditor/Classes/Controller/BrowseLinksController.php [new file with mode: 0644]
typo3/sysext/rte_ckeditor/Classes/Form/Element/RichTextElement.php
typo3/sysext/rte_ckeditor/Configuration/Backend/Routes.php [new file with mode: 0644]
typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/Contrib/plugins/typo3link/plugin.js [new file with mode: 0644]
typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/RteLinkBrowser.js [new file with mode: 0644]
typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/defaultconfig.js

index 7b2780b..be1868e 100644 (file)
@@ -45,6 +45,9 @@
                        <trans-unit id="target">
                                <source>Target</source>
                        </trans-unit>
+                       <trans-unit id="linkRelationship">
+                               <source>Relationship</source>
+                       </trans-unit>
                        <trans-unit id="class">
                                <source>CSS-Class</source>
                        </trans-unit>
diff --git a/typo3/sysext/rte_ckeditor/Classes/Controller/BrowseLinksController.php b/typo3/sysext/rte_ckeditor/Classes/Controller/BrowseLinksController.php
new file mode 100644 (file)
index 0000000..f6bbe76
--- /dev/null
@@ -0,0 +1,572 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\RteCKEditor\Controller;
+
+/*
+ * 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 Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\LinkHandling\LinkService;
+use TYPO3\CMS\Core\Page\PageRenderer;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Lang\LanguageService;
+use TYPO3\CMS\Recordlist\Controller\AbstractLinkBrowserController;
+
+/**
+ * Extended controller for link browser
+ */
+class BrowseLinksController extends AbstractLinkBrowserController
+{
+    /**
+     * @var string
+     */
+    protected $editorId;
+
+    /**
+     * TYPO3 language code of the content language
+     *
+     * @var int
+     */
+    protected $contentTypo3Language;
+
+    /**
+     * Language service object for localization to the content language
+     *
+     * @var LanguageService
+     */
+    protected $contentLanguageService;
+
+    /**
+     * @var array
+     */
+    protected $buttonConfig = [];
+
+    /**
+     * @var array
+     */
+    protected $thisConfig = [];
+
+    /**
+     * RTE configuration
+     *
+     * @var array
+     */
+    protected $RTEProperties = [];
+
+    /**
+     * Used with the Rich Text Editor.
+     * Example value: "tt_content:NEW3fba58c969f5c:bodytext:23:text:23:"
+     *
+     * @var string
+     */
+    protected $RTEtsConfigParams;
+
+    /**
+     * @var array
+     */
+    protected $classesAnchorDefault = [];
+
+    /**
+     * @var array
+     */
+    protected $classesAnchorDefaultTitle = [];
+
+    /**
+     * @var array
+     */
+    protected $classesAnchorClassTitle = [];
+
+    /**
+     * @var array
+     */
+    protected $classesAnchorDefaultTarget = [];
+
+    /**
+     * @var array
+     */
+    protected $classesAnchorJSOptions = [];
+
+    /**
+     * @var string
+     */
+    protected $defaultLinkTarget = '';
+
+    /**
+     * @var array
+     */
+    protected $additionalAttributes = [];
+
+    /**
+     * @var string
+     */
+    protected $siteUrl = '';
+
+    /**
+     * Initialize controller
+     */
+    protected function init()
+    {
+        parent::init();
+
+        $this->contentLanguageService = GeneralUtility::makeInstance(LanguageService::class);
+    }
+
+    /**
+     * @param ServerRequestInterface $request
+     */
+    protected function initVariables(ServerRequestInterface $request)
+    {
+        parent::initVariables($request);
+
+        $queryParameters = $request->getQueryParams();
+
+        $this->siteUrl = GeneralUtility::getIndpEnv('TYPO3_SITE_URL');
+
+        $currentLinkParts = isset($queryParameters['curUrl']) ? $queryParameters['curUrl'] : [];
+        $this->currentLinkParts = $currentLinkParts;
+        $this->editorId = GeneralUtility::_GP('editorId');
+        $this->contentTypo3Language = GeneralUtility::_GP('contentTypo3Language');
+        $this->RTEtsConfigParams = GeneralUtility::_GP('RTEtsConfigParams');
+
+        $this->contentLanguageService->init($this->contentTypo3Language);
+
+        $RTEtsConfigParts = explode(':', $this->RTEtsConfigParams);
+        $RTEsetup = $this->getBackendUser()->getTSConfig('RTE', BackendUtility::getPagesTSconfig($RTEtsConfigParts[5]));
+        $this->RTEProperties = $RTEsetup['properties'];
+
+        $this->thisConfig = BackendUtility::RTEsetup($this->RTEProperties, $RTEtsConfigParts[0], $RTEtsConfigParts[2], $RTEtsConfigParts[4]);
+        $this->buttonConfig = $this->thisConfig['buttons.']['link.'] ?? [];
+    }
+
+    /**
+     * Initialize document template object
+     *
+     *  @return void
+     */
+    protected function initDocumentTemplate()
+    {
+        parent::initDocumentTemplate();
+
+        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
+        $pageRenderer->loadRequireJsModule(
+            'TYPO3/CMS/RteCkeditor/RteLinkBrowser',
+            'function(RteLinkBrowser) {
+                RteLinkBrowser.initialize(' . GeneralUtility::quoteJSvalue($this->editorId) . ');
+            }'
+        );
+    }
+
+    /**
+     * Initialize $this->currentLink and $this->currentLinkHandler
+     *
+     * @return void
+     */
+    protected function initCurrentUrl()
+    {
+        if (empty($this->currentLinkParts)) {
+            return;
+        }
+
+        if (!empty($this->currentLinkParts['url'])) {
+            $linkService = GeneralUtility::makeInstance(LinkService::class);
+            $data = $linkService->resolve($this->currentLinkParts['url']);
+            $this->currentLinkParts['type'] = $data['type'];
+            unset($data['type']);
+            $this->currentLinkParts['url'] = $data;
+        }
+
+        if (!empty($this->currentLinkParts['class'])) {
+            // Only keep last class value (others are automatically added again by required option)
+            // https://review.typo3.org/#/c/29643
+            $currentClasses = GeneralUtility::trimExplode(' ', $this->currentLinkParts['class'], true);
+            if (count($currentClasses) > 1) {
+                $this->currentLinkParts['class'] = end($currentClasses);
+            }
+        }
+        parent::initCurrentUrl();
+    }
+
+    /**
+     * Renders the link attributes for the selected link handler
+     *
+     * @return string
+     */
+    public function renderLinkAttributeFields()
+    {
+        // Processing the classes configuration
+        if (!empty($this->buttonConfig['properties.']['class.']['allowedClasses'])) {
+            $classesAnchorArray = GeneralUtility::trimExplode(',', $this->buttonConfig['properties.']['class.']['allowedClasses'], true);
+            // Collecting allowed classes and configured default values
+            $classesAnchor = [
+                'all' => []
+            ];
+            $titleReadOnly = $this->buttonConfig['properties.']['title.']['readOnly']
+                || $this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['title.']['readOnly'];
+            if (is_array($this->RTEProperties['classesAnchor.'])) {
+                foreach ($this->RTEProperties['classesAnchor.'] as $label => $conf) {
+                    if (in_array($conf['class'], $classesAnchorArray, true)) {
+                        $classesAnchor['all'][] = $conf['class'];
+                        if ($conf['type'] === $this->displayedLinkHandlerId) {
+                            $classesAnchor[$conf['type']][] = $conf['class'];
+                            if ($this->buttonConfig[$conf['type'] . '.']['properties.']['class.']['default'] == $conf['class']) {
+                                $this->classesAnchorDefault[$conf['type']] = $conf['class'];
+                                if ($conf['titleText']) {
+                                    $this->classesAnchorDefaultTitle[$conf['type']] = $this->contentLanguageService->sL(trim($conf['titleText']));
+                                }
+                                if (isset($conf['target'])) {
+                                    $this->classesAnchorDefaultTarget[$conf['type']] = trim($conf['target']);
+                                }
+                            }
+                        }
+                        if ($titleReadOnly && $conf['titleText']) {
+                            $this->classesAnchorClassTitle[$conf['class']] = ($this->classesAnchorDefaultTitle[$conf['type']] = $this->contentLanguageService->sL(trim($conf['titleText'])));
+                        }
+                    }
+                }
+            }
+            if (isset($this->linkAttributeValues['class'])
+                && isset($classesAnchor[$this->displayedLinkHandlerId])
+                && !in_array($this->linkAttributeValues['class'], $classesAnchor[$this->displayedLinkHandlerId], true)
+            ) {
+                unset($this->linkAttributeValues['class']);
+            }
+            // Constructing the class selector options
+            foreach ($classesAnchorArray as $class) {
+                if (!in_array($class, $classesAnchor['all']) || in_array($class, $classesAnchor['all']) && is_array($classesAnchor[$this->displayedLinkHandlerId]) && in_array($class, $classesAnchor[$this->displayedLinkHandlerId])) {
+                    $selected = '';
+                    if ($this->linkAttributeValues['class'] === $class || !$this->linkAttributeValues['class'] && $this->classesAnchorDefault[$this->displayedLinkHandlerId] == $class) {
+                        $selected = 'selected="selected"';
+                    }
+                    $classLabel = !empty($this->RTEProperties['classes.'][$class . '.']['name'])
+                        ? $this->getPageConfigLabel($this->RTEProperties['classes.'][$class . '.']['name'], 0)
+                        : $class;
+                    $classStyle = !empty($this->RTEProperties['classes.'][$class . '.']['value'])
+                        ? $this->RTEProperties['classes.'][$class . '.']['value']
+                        : '';
+                    $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] .= '<option ' . $selected . ' value="' . $class . '"' . ($classStyle ? ' style="' . $classStyle . '"' : '') . '>' . $classLabel . '</option>';
+                }
+            }
+            if ($this->classesAnchorJSOptions[$this->displayedLinkHandlerId] && !($this->buttonConfig['properties.']['class.']['required'] || $this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['class.']['required'])) {
+                $selected = '';
+                if (!$this->linkAttributeValues['class'] && !$this->classesAnchorDefault[$this->displayedLinkHandlerId]) {
+                    $selected = 'selected="selected"';
+                }
+                $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] = '<option ' . $selected . ' value=""></option>' . $this->classesAnchorJSOptions[$this->displayedLinkHandlerId];
+            }
+        }
+        // Default target
+        $this->defaultLinkTarget = $this->classesAnchorDefault[$this->displayedLinkHandlerId] && $this->classesAnchorDefaultTarget[$this->displayedLinkHandlerId]
+            ? $this->classesAnchorDefaultTarget[$this->displayedLinkHandlerId]
+            : (isset($this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['target.']['default'])
+                ? $this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['target.']['default']
+                : (isset($this->buttonConfig['properties.']['target.']['default'])
+                    ? $this->buttonConfig['properties.']['target.']['default']
+                    : ''));
+
+        // todo: find new name for this option
+        // Initializing additional attributes
+        if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rtehtmlarea']['plugins']['TYPO3Link']['additionalAttributes']) {
+            $addAttributes = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rtehtmlarea']['plugins']['TYPO3Link']['additionalAttributes'], true);
+            foreach ($addAttributes as $attribute) {
+                $this->additionalAttributes[$attribute] = $this->linkAttributeValues[$attribute] ?? '';
+            }
+        }
+        return parent::renderLinkAttributeFields();
+    }
+
+    /**
+     * Localize a label obtained from Page TSConfig
+     *
+     * @param string $string The label to be localized
+     * @param bool $JScharCode If needs to be converted to an array of char numbers
+     * @return string Localized string
+     */
+    public function getPageConfigLabel($string, $JScharCode = true)
+    {
+        if (strpos($string, 'LLL:') !== 0) {
+            $label = $string;
+        } else {
+            $label = $this->getLanguageService()->sL(trim($string));
+        }
+        $label = str_replace('"', '\\"', str_replace('\\\'', '\'', $label));
+        return $JScharCode ? GeneralUtility::quoteJSvalue($label) : $label;
+    }
+
+    /**
+     * @return string
+     */
+    protected function renderCurrentUrl()
+    {
+        $removeLink = ' <a href="#" class="btn btn-default t3js-removeCurrentLink">' . htmlspecialchars($this->getLanguageService()->getLL('removeLink')) . '</a>';
+        return '
+            <div class="link-browser-section link-browser-current-link">
+                <strong>' .
+                    htmlspecialchars($this->getLanguageService()->getLL('currentLink')) .
+                    ': ' .
+                    htmlspecialchars($this->currentLinkHandler->formatCurrentUrl()) .
+                '</strong>' .
+                '<span class="pull-right">' . $removeLink . '</span>' .
+            '</div>';
+    }
+
+    /**
+     * Get the allowed items or tabs
+     *
+     * @return string[]
+     */
+    protected function getAllowedItems()
+    {
+        $allowedItems = parent::getAllowedItems();
+
+        $blindLinkOptions = isset($this->thisConfig['blindLinkOptions'])
+            ? GeneralUtility::trimExplode(',', $this->thisConfig['blindLinkOptions'], true)
+            : [];
+        $allowedItems = array_diff($allowedItems, $blindLinkOptions);
+
+        if (is_array($this->buttonConfig['options.']) && $this->buttonConfig['options.']['removeItems']) {
+            $allowedItems = array_diff($allowedItems, GeneralUtility::trimExplode(',', $this->buttonConfig['options.']['removeItems'], true));
+        }
+
+        return $allowedItems;
+    }
+
+    /**
+     * Get the allowed link attributes
+     *
+     * @return string[]
+     */
+    protected function getAllowedLinkAttributes()
+    {
+        $allowedLinkAttributes = parent::getAllowedLinkAttributes();
+
+        $blindLinkFields = isset($this->thisConfig['blindLinkFields'])
+            ? GeneralUtility::trimExplode(',', $this->thisConfig['blindLinkFields'], true)
+            : [];
+        $allowedLinkAttributes = array_diff($allowedLinkAttributes, $blindLinkFields);
+
+        return $allowedLinkAttributes;
+    }
+
+    /**
+     * Create an array of link attribute field rendering definitions
+     *
+     * @return string[]
+     */
+    protected function getLinkAttributeFieldDefinitions()
+    {
+        $fieldRenderingDefinitions = parent::getLinkAttributeFieldDefinitions();
+        $fieldRenderingDefinitions['title'] = $this->getTitleField();
+        $fieldRenderingDefinitions['class'] = $this->getClassField();
+        $fieldRenderingDefinitions['target'] = $this->getTargetField();
+        $fieldRenderingDefinitions['rel'] = $this->getRelField();
+        if (empty($this->buttonConfig['queryParametersSelector.']['enabled'])) {
+            unset($fieldRenderingDefinitions['params']);
+        }
+        return $fieldRenderingDefinitions;
+    }
+
+    /**
+     * Add rel field
+     *
+     * @return string
+     */
+    protected function getRelField()
+    {
+        if (empty($this->buttonConfig['relAttribute.']['enabled'])) {
+            return '';
+        }
+
+        $currentRel = $this->displayedLinkHandler === $this->currentLinkHandler && !empty($this->currentLinkParts)
+            ? $this->linkAttributeValues['rel']
+            : '';
+
+        return '
+            <form action="" name="lrelform" id="lrelform" class="t3js-dummyform form-horizontal">
+                 <div class="form-group form-group-sm">
+                    <label class="col-xs-4 control-label">' .
+                        htmlspecialchars($this->getLanguageService()->getLL('linkRelationship')) .
+                    '</label>
+                    <div class="col-xs-8">
+                        <input type="text" name="lrel" class="form-control" value="' . $currentRel . '" />
+                    </div>
+                </div>
+            </form>
+            ';
+    }
+
+    /**
+     * Add target selector
+     *
+     * @return string
+     */
+    protected function getTargetField()
+    {
+        $targetSelectorConfig = [];
+        if (is_array($this->buttonConfig['targetSelector.'])) {
+            $targetSelectorConfig = $this->buttonConfig['targetSelector.'];
+        }
+        $target = $this->linkAttributeValues['target'] ?: $this->defaultLinkTarget;
+        $lang = $this->getLanguageService();
+        $targetSelector = '';
+
+        if (!$targetSelectorConfig['disabled']) {
+            $targetSelector = '
+                                               <select name="ltarget_type" class="t3js-targetPreselect form-control">
+                                                       <option value=""></option>
+                                                       <option value="_top">' . htmlspecialchars($lang->getLL('top')) . '</option>
+                                                       <option value="_blank">' . htmlspecialchars($lang->getLL('newWindow')) . '</option>
+                                               </select>
+                       ';
+        }
+
+        return '
+                               <form action="" name="ltargetform" id="ltargetform" class="t3js-dummyform form-horizontal">
+                    <div class="form-group form-group-sm" ' . ($targetSelectorConfig['disabled'] ? ' style="display: none;"' : '') . '>
+                        <label class="col-xs-4 control-label">' . htmlspecialchars($lang->getLL('target')) . '</label>
+                                               <div class="col-xs-3">
+                                                       <input type="text" name="ltarget" class="t3js-linkTarget form-control"
+                                                           value="' . htmlspecialchars($target) . '" />
+                                               </div>
+                                               <div class="col-xs-3">
+                                                       ' . $targetSelector . '
+                                               </div>
+                                       </div>
+                               </form>
+                               ';
+    }
+
+    /**
+     * Add title selector
+     *
+     * @return string
+     */
+    protected function getTitleField()
+    {
+        if ($this->linkAttributeValues['title']) {
+            $title = $this->linkAttributeValues['title'];
+        } else {
+            $title = !$this->classesAnchorDefault[$this->displayedLinkHandlerId] ? '' : $this->classesAnchorDefaultTitle[$this->displayedLinkHandlerId];
+        }
+        if (isset($this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['title.']['readOnly'])) {
+            $readOnly = (bool)$this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['title.']['readOnly'];
+        } else {
+            $readOnly = isset($this->buttonConfig['properties.']['title.']['readOnly'])
+                ? (bool)$this->buttonConfig['properties.']['title.']['readOnly']
+                : false;
+        }
+
+        if ($readOnly) {
+            $currentClass = $this->linkAttributeFields['class'];
+            if (!$currentClass) {
+                $currentClass = empty($this->classesAnchorDefault[$this->displayedLinkHandlerId]) ? '' : $this->classesAnchorDefault[$this->displayedLinkHandlerId];
+            }
+            $title = $currentClass
+                ? $this->classesAnchorClassTitle[$currentClass]
+                : $this->classesAnchorDefaultTitle[$this->displayedLinkHandlerId];
+        }
+        return '
+                <form action="" name="ltitleform" id="ltitleform" class="t3js-dummyform form-horizontal">
+                    <div class="form-group form-group-sm">
+                        <label class="col-xs-4 control-label">
+                            ' . htmlspecialchars($this->getLanguageService()->getLL('title')) . '
+                         </label>
+                         <div class="col-xs-8">
+                                <span style="display: ' . ($readOnly ? 'none' : 'inline') . ';">
+                                    <input type="text" name="ltitle" class="form-control"
+                                        value="' . htmlspecialchars($title) . '" />
+                                </span>
+                                <span id="rte-ckeditor-browse-links-title-readonly"
+                                    style="display: ' . ($readOnly ? 'inline' : 'none') . ';">
+                                    ' . htmlspecialchars($title) . '</span>
+                        </div>
+                    </div>
+                </form>
+                ';
+    }
+
+    /**
+     * Return html code for the class selector
+     *
+     * @return string the html code to be added to the form
+     */
+    protected function getClassField()
+    {
+        $selectClass = '';
+        if ($this->classesAnchorJSOptions[$this->displayedLinkHandlerId]) {
+            $selectClass = '
+                               <form action="" name="lclassform" id="lclassform" class="t3js-dummyform">
+                                       <table border="0" cellpadding="2" cellspacing="1" id="typo3-linkClass">
+                                               <tr>
+                                                       <td style="width: 96px;">' . htmlspecialchars($this->getLanguageService()->getLL('class')) . '</td>
+                                                       <td><select name="lclass" class="t3js-class-selector form-control">
+                                                               ' . $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] . '
+                                                       </select></td>
+                                               </tr>
+                                       </table>
+                               </form>
+                               ';
+        }
+        return $selectClass;
+    }
+
+    /**
+     * Return the ID of current page
+     *
+     * @return int
+     */
+    protected function getCurrentPageId()
+    {
+        return explode(':', $this->RTEtsConfigParams)[5];
+    }
+
+    /**
+     * Retrieve the configuration
+     *
+     * This is only used by RTE currently.
+     *
+     * @return array
+     */
+    public function getConfiguration()
+    {
+        return $this->buttonConfig;
+    }
+
+    /**
+     * Get attributes for the body tag
+     *
+     * @return string[] Array of body-tag attributes
+     */
+    protected function getBodyTagAttributes()
+    {
+        $parameters = parent::getBodyTagAttributes();
+        $parameters['data-site-url'] = $this->siteUrl;
+        $parameters['data-default-link-target'] = $this->defaultLinkTarget;
+        return $parameters;
+    }
+
+    /**
+     * @param array $overrides
+     *
+     * @return array Array of parameters which have to be added to URLs
+     */
+    public function getUrlParameters(array $overrides = null)
+    {
+        return [
+            'act' => isset($overrides['act']) ? $overrides['act'] : $this->displayedLinkHandlerId,
+            'editorId' => $this->editorId,
+            'contentTypo3Language' => $this->contentTypo3Language,
+            'RTEtsConfigParams' => $this->RTEtsConfigParams,
+        ];
+    }
+}
index 8b98650..aade0bc 100644 (file)
@@ -19,6 +19,7 @@ use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Lang\LanguageService;
 
@@ -29,13 +30,36 @@ class RichTextElement extends AbstractFormElement
 {
 
     /**
+     * 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;
+
+    /**
+     * RTE configuration
+     * This property contains "processed" configuration
+     * where table and type specific RTE setup is merged into 'default.' array.
+     *
+     * @var array
+     */
+    protected $rteConfiguration = [];
+
+    /**
      * Renders the ckeditor element
      *
      * @return array
+     * @throws \InvalidArgumentException
      */
     public function render() : array
     {
         $resultArray = $this->initializeResultArray();
+        $row = $this->data['databaseRow'];
+        BackendUtility::fixVersioningPid($this->data['tableName'], $row);
+        $this->pidOfVersionedMotherRecord = (int)$row['pid'];
 
         $resourcesPath = PathUtility::getAbsoluteWebPath(
             ExtensionManagementUtility::extPath('rte_ckeditor', 'Resources/Public/')
@@ -58,6 +82,15 @@ class RichTextElement extends AbstractFormElement
             $defaultExtras,
             true
         );
+
+        $vanillaRteTsConfig = $this->getBackendUserAuthentication()->getTSConfig('RTE', BackendUtility::getPagesTSconfig($this->data['effectivePid']));
+        $this->rteConfiguration = BackendUtility::RTEsetup(
+            $vanillaRteTsConfig['properties'],
+            $table,
+            $this->data['fieldName'],
+            $this->data['recordTypeValue']
+        );
+
         $resultArray['requireJsModules'] = [];
         $resultArray['requireJsModules'][] =[
             'ckeditor' => $this->getCkEditorRequireJsModuleCode($resourcesPath, $fieldId)
@@ -75,17 +108,33 @@ class RichTextElement extends AbstractFormElement
      */
     protected function getCkEditorRequireJsModuleCode(string $resourcesPath, string $fieldId) : string
     {
+        // todo: find new name for this option (do we still need this?)
+        // Initializing additional attributes
+        $additionalAttributes = [];
+        if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rtehtmlarea']['plugins']['TYPO3Link']['additionalAttributes']) {
+            $additionalAttributes = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rtehtmlarea']['plugins']['TYPO3Link']['additionalAttributes'], true);
+        }
+
+        $customConfig = [
+            'contentsCss' => $resourcesPath . 'Css/contents.css',
+            'customConfig' => $resourcesPath . 'JavaScript/defaultconfig.js',
+            'toolbar' => 'Basic',
+            'uiColor' => '#F8F8F8',
+            'stylesSet' => 'default',
+            'RTEtsConfigParams' => $this->getRTEtsConfigParams(),
+            'elementbrowser' => [
+                'link' => [
+                    'moduleUrl' => BackendUtility::getModuleUrl('rteckeditor_wizard_browse_links'),
+                    'additionalAttributes' => $additionalAttributes
+                ]
+            ]
+        ];
+
         return 'function(CKEDITOR) {
                 CKEDITOR.config.height = 400;
                 CKEDITOR.contentsCss = "' . $resourcesPath . 'Css/contents.css";
                 CKEDITOR.config.width = "auto";
-                CKEDITOR.replace("' . $fieldId . '", {
-                    contentsCss: "' . $resourcesPath . 'Css/contents.css",
-                    customConfig: "' . $resourcesPath . 'JavaScript/defaultconfig.js",
-                    toolbar : "Basic",
-                    uiColor : "#F8F8F8",
-                    stylesSet: "default"
-                });
+                CKEDITOR.replace("' . $fieldId . '", ' . json_encode($customConfig) . ');
         }';
     }
 
@@ -104,6 +153,24 @@ class RichTextElement extends AbstractFormElement
     }
 
     /**
+     * A list of parameters that is mostly given as GET/POST to other RTE controllers.
+     *
+     * @return string
+     */
+    protected function getRTEtsConfigParams() : string
+    {
+        $result = [
+            $this->data['tableName'],
+            $this->data['databaseRow']['uid'],
+            $this->data['fieldName'],
+            $this->pidOfVersionedMotherRecord,
+            $this->data['recordTypeValue'],
+            $this->data['effectivePid'],
+        ];
+        return implode(':', $result);
+    }
+
+    /**
      * @return LanguageService
      */
     protected function getLanguageService() : LanguageService
diff --git a/typo3/sysext/rte_ckeditor/Configuration/Backend/Routes.php b/typo3/sysext/rte_ckeditor/Configuration/Backend/Routes.php
new file mode 100644 (file)
index 0000000..fe2c8a3
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * Definitions of routes
+ */
+return [
+    // Register RTE browse links wizard
+    'rteckeditor_wizard_browse_links' => [
+        'path' => '/rte/wizard/browselinks',
+        'target' => \TYPO3\CMS\RteCKEditor\Controller\BrowseLinksController::class . '::mainAction'
+    ],
+];
diff --git a/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/Contrib/plugins/typo3link/plugin.js b/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/Contrib/plugins/typo3link/plugin.js
new file mode 100644 (file)
index 0000000..ae48c57
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * 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 strict';
+
+(function () {
+
+       CKEDITOR.plugins.add('typo3link', {
+               elementBrowser: null,
+               init: function (editor) {
+                       var allowed = 'a[!href,title,class,target,rel]',
+                               required = 'a[href]';
+
+                       if (editor.config.elementbrowser.link.additionalAttributes && editor.config.elementbrowser.link.additionalAttributes.length) {
+                               allowed = allowed.replace( ']', ',' + editor.config.elementbrowser.link.additionalAttributes.join(',') + ']');
+                       }
+
+                       // Override link command
+                       editor.addCommand('link', {
+                               exec: openLinkBrowser,
+                               allowedContent: allowed,
+                               requiredContent: required
+                       });
+
+                       // Override doubleclick opening default link dialog
+                       editor.on('doubleclick', function (evt) {
+                               var element = CKEDITOR.plugins.link.getSelectedLink(editor) || evt.data.element;
+                               if (!element.isReadOnly() && element.is('a') && element.getAttribute('href')) {
+                                       evt.stop();
+                                       openLinkBrowser(editor, element);
+                               }
+                       }, null, null, 30);
+
+               }
+       });
+
+       /**
+        * Open link browser
+        *
+        * @param {Object} editor CKEditor object
+        * @param {Object} element Selected link element
+        */
+       function openLinkBrowser(editor, element) {
+               var additionalParameters = '';
+
+               if (!element) {
+                       element = CKEDITOR.plugins.link.getSelectedLink(editor);
+               }
+               if (element) {
+                       additionalParameters = '&curUrl[url]=' + encodeURIComponent(element.getAttribute('href'));
+                       var i = 0,
+                               attributeNames = ["target", "class", "title", "rel"];
+                       for (i = 0; i < attributeNames.length; ++i) {
+                               if (element.getAttribute(attributeNames[i])) {
+                                       additionalParameters += '&curUrl[' + attributeNames[i] + ']=';
+                                       additionalParameters += encodeURIComponent(element.getAttribute(attributeNames[i]));
+                               }
+                       }
+
+                       var additionalAttributes = editor.config.elementbrowser.link.additionalAttributes;
+                       for (i = additionalAttributes.length; --i >= 0;) {
+                               if (element.hasAttribute(additionalAttributes[i])) {
+                                       additionalParameters += '&curUrl[' + additionalAttributes[i] + ']=';
+                                       additionalParameters += encodeURIComponent(element.getAttribute(additionalAttributes[i]));
+                               }
+                       }
+               }
+
+               openElementBrowser(
+                       editor,
+                       editor.lang.link.toolbar,
+                       TYPO3.settings.Textarea.RTEPopupWindow.height - 20,
+                       makeUrlFromModulePath(
+                               editor,
+                               editor.config.elementbrowser.link.moduleUrl,
+                               additionalParameters
+                       ));
+       }
+
+       /**
+        * Make url from module path
+        *
+        * @param {Object} editor CKEditor object
+        * @param {String} modulePath Module path
+        * @param {String} parameters Additional parameters
+        *
+        * @return {String} The url
+        */
+       function makeUrlFromModulePath(editor, modulePath, parameters) {
+
+               // todo: check if we need `+ '&contentTypo3Language=' + this.editorConfiguration.typo3ContentLanguage `
+               return modulePath
+                       + (modulePath.indexOf("?") === -1 ? "?" : "&")
+                       + 'RTEtsConfigParams=' + editor.config.RTEtsConfigParams
+                       + '&editorId=' + editor.id
+                       + (parameters ? parameters : '');
+       }
+
+       /**
+        * Open a window with container iframe
+        *
+        * @param {Object} editor The CKEditor instance
+        * @param {String} title The window title (will be localized here)
+        * @param {Integer} height The height of the containing iframe
+        * @param {String} url The url to load ino the iframe
+        */
+       function openElementBrowser(editor, title, height, url) {
+               require([
+                       'jquery',
+                       'TYPO3/CMS/Backend/Modal',
+                       'TYPO3/CMS/Backend/Severity'
+                       ], function ($, Modal, Severity) {
+
+                       var $iframe = $('<iframe />', {
+                                       src: url,
+                                       'class': 'content-iframe',
+                                       style: 'border: 0; width: 100%; height: ' + height * 1 + 'px;'
+                               }),
+                               $content = $('<div />', {'class': 'rte-ckeditor-window', id: editor.id}).append($iframe);
+
+                       var elementBrowser = Modal.show(title, $content, Severity.notice);
+
+                       // TODO: add this to less/css (.rte-ckeditor-window .modal-body)
+                       //               further, make modal wider and maybe resize-able
+                       elementBrowser.find('.modal-body').css('padding', 0);
+
+               });
+       }
+
+})();
diff --git a/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/RteLinkBrowser.js b/typo3/sysext/rte_ckeditor/Resources/Public/JavaScript/RteLinkBrowser.js
new file mode 100644 (file)
index 0000000..e108ac3
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * 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!
+ */
+
+/**
+ * Module: TYPO3/CMS/RteCkeditor/RteLinkBrowser
+ * LinkBrowser communication with parent window
+ */
+define(['jquery', 'TYPO3/CMS/Recordlist/LinkBrowser', 'TYPO3/CMS/Backend/Modal'], function ($, LinkBrowser, Modal) {
+       'use strict';
+
+       /**
+        *
+        * @type {{plugin: null, CKEditor: null, siteUrl: string}}
+        * @exports TYPO3/CMS/RteCkeditor/RteLinkBrowser
+        */
+       var RteLinkBrowser = {
+               plugin: null,
+               CKEditor: null,
+               siteUrl: ''
+       };
+
+       /**
+        * @param {String} editorId Id of CKEditor
+        */
+       RteLinkBrowser.initialize = function (editorId) {
+               var callerWindow;
+               if (typeof top.TYPO3.Backend !== 'undefined' && typeof top.TYPO3.Backend.ContentContainer.get() !== 'undefined') {
+                       callerWindow = top.TYPO3.Backend.ContentContainer.get();
+               } else {
+                       callerWindow = window.parent;
+               }
+
+               $.each(callerWindow.CKEDITOR.instances, function (name, editor) {
+                       if (editor.id === editorId) {
+                               RteLinkBrowser.CKEditor = editor;
+                       }
+               });
+
+               // siteUrl etc are added as data attributes to the body tag
+               $.extend(RteLinkBrowser, $('body').data());
+
+               $('.t3js-removeCurrentLink').on('click', function (event) {
+                       event.preventDefault();
+                       RteLinkBrowser.CKEditor.execCommand('unlink');
+                       Modal.dismiss();
+               });
+       };
+
+       /**
+        * Store the final link
+        *
+        * @param {String} link The select element or anything else which identifies the link (e.g. "page:<pageUid>" or "file:<uid>")
+        */
+       LinkBrowser.finalizeFunction = function (link) {
+
+               var linkElement = RteLinkBrowser.CKEditor.document.createElement('a');
+               var attributes = LinkBrowser.getLinkAttributeValues();
+               var params = attributes.params ? attributes.params : '';
+
+               if (attributes.target) {
+                       linkElement.setAttribute('target', attributes.target);
+               }
+               if (attributes.class) {
+                       linkElement.setAttribute('class', attributes.class);
+               }
+               if (attributes.title) {
+                       linkElement.setAttribute('title', attributes.title);
+               }
+               delete attributes.title;
+               delete attributes.class;
+               delete attributes.target;
+               delete attributes.params;
+
+               $.each(attributes, function (attrName, attrValue) {
+                       linkElement.setAttribute(attrName, attrValue);
+               });
+
+               // todo: check if this still needed?
+               // if there is no handler keyword (mailto:, record:, etc) or an external link, we always prepend the siteUrl
+               if (!/^\w+:/.test(link) && !attributes['data-htmlarea-external']) {
+                       link = RteLinkBrowser.siteUrl + '?' + link;
+               }
+
+               linkElement.setAttribute('href', link + params);
+
+               var selection = RteLinkBrowser.CKEditor.getSelection();
+               if (selection && selection.getSelectedText()) {
+                       linkElement.setText(selection.getSelectedText());
+               } else {
+                       linkElement.setText(linkElement.getAttribute('href'));
+               }
+               RteLinkBrowser.CKEditor.insertElement(linkElement);
+
+               Modal.dismiss();
+       };
+
+       return RteLinkBrowser;
+});
index 51156c6..857a58f 100644 (file)
@@ -21,7 +21,7 @@ CKEDITOR.editorConfig = function( config ) {
                { name: 'insert' },
                { name: 'forms' },
                { name: 'tools' },
-               { name: 'document',        groups: [ 'mode', 'document', 'doctools' ] },
+               { name: 'document',    groups: [ 'mode', 'document', 'doctools' ] },
                { name: 'others' },
                '/',
                { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
@@ -33,13 +33,15 @@ CKEDITOR.editorConfig = function( config ) {
 
        // Remove some buttons provided by the standard plugins, which are
        // not needed in the Standard(s) toolbar.
-       config.removeButtons = 'Underline,Subscript,Superscript';
+       config.removeButtons = 'Underline,Subscript,Superscript,Anchor';
 
        // Set the most common block elements.
        config.format_tags = 'p;h1;h2;h3;pre';
 
        // Simplify the dialog windows.
        config.removeDialogTabs = 'image:advanced;link:advanced';
+
+       config.extraPlugins = 'typo3link';
 };
 
 CKEDITOR.stylesSet.add('default', [