diff --git a/Classes/Backend/BackendJsCss.php b/Classes/Backend/BackendJsCss.php new file mode 100644 index 0000000..eb501a1 --- /dev/null +++ b/Classes/Backend/BackendJsCss.php @@ -0,0 +1,31 @@ +getMajorVersion(); + $ref->addCssFile('EXT:simplereference/Resources/Public/Backend/Css/styles.css'); + if ($typo3Version === 10) { + $ref->addJsFooterFile('EXT:simplereference/Resources/Public/JavaScript/Main.v10.js'); + } + if ($typo3Version === 11) { + $ref->addJsFooterFile('EXT:simplereference/Resources/Public/JavaScript/Main.v11.js'); + } + + $ref->addInlineLanguageLabelFile('EXT:simplereference/Resources/Private/Language/locallang_modal.xlf'); + } + } +} diff --git a/Classes/Backend/Helper.php b/Classes/Backend/Helper.php new file mode 100644 index 0000000..86ee661 --- /dev/null +++ b/Classes/Backend/Helper.php @@ -0,0 +1,203 @@ +getParsedBody(): + * + * Add new reference on top of a normal colPos + * [addPanelId] = colpos-1-page-50-62c20b4d2e903078051133 // insert panel HTML Id + * [addColpos] => 1 // colPos + * [addPageUid] => 50 // page uid + * [addLanguageuid] => 0 // sys_language_uid + * + * Add new reference on top of a container element + * [addPanelId] = colpos-201-page-50-62c20b4d2e903078051133 // insert panel HTML Id + * [addColpos] => 283-201 + * [addPageUid] => 50 // page uid + * [addLanguageuid] => 0 + * + * Add a new reference below existing content element 'addUid' : contentElementUid + * [addPanelId] = colpos-201-page-50-62c20b4d2e903078051133 // insert panel HTML Id + * [addTable] => tt_content + * [addUid] => 281 + * [addPageUid] => 50 // page uid + * [addLanguageuid] => 0 + * + * @param ServerRequestInterface $request + * @return ResponseInterface + * @throws RouteNotFoundException + */ + public function getData(ServerRequestInterface $request): ResponseInterface + { + $data = ['info' => 'null']; + $beUser = $this->getBackendUser(); + + // no backend user? return... + if (!is_object($beUser)) { + return new JsonResponse($data); + } + + // the incoming get or post + $parsedBody = $request->getParsedBody(); + $references = $this->getReferences($request); + + // early return if we have nothing to add + if (!count($references)) { + return new JsonResponse($data); + } + $data['references'] = $references; + + + if (isset($parsedBody['addReference'])) { + + $records = implode(',', $references); + $newUid = (microtime(true) * 10000); + $infoArray = GeneralUtility::intExplode('-',$parsedBody['addPanelId'],true); + + if (isset($parsedBody['addTable'])) { + $pid = '-' . $parsedBody['addUid']; + } else { + $pid = $parsedBody['addPageUid']; + } + + $data['data'] = [ + 'tt_content' => [ + 'NEW' . $newUid => [ + 'CType' => 'shortcut', + 'header' => 'Reference', + //'header_layout' => '100', + 'colPos' => (int)$infoArray[1], //end($colPosArray), + 'sys_language_uid' => (int)$parsedBody['addLanguageuid'], + 'pid' => (int)$pid, + 'records' => $records, + ], + ], + ]; + + // add EXT:container parent + if (isset($parsedBody['addTable'])) { + $currentRecord = $this->getRecordByUid((int)$parsedBody['addUid']); + if (array_key_exists('tx_container_parent', $currentRecord)) { + $data['data']['tt_content']['NEW' . $newUid]['tx_container_parent'] = $currentRecord['tx_container_parent']; + } + } + + // or override if we are on top of a EXT:container colPos + if (isset($parsedBody['addColpos'])) { + $colPosArray = GeneralUtility::intExplode('-', $parsedBody['addColpos'], true); + if (count($colPosArray) === 2) { + $data['data']['tt_content']['NEW' . $newUid]['tx_container_parent'] = $colPosArray[0]; + } + } + + // create redirect uri + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $route = 'web_layout'; + $parameters = [ + 'id' => $parsedBody['addPageUid'], + 't' => time(), + ]; + $referenceType = ''; + $data['redirect'] = $uriBuilder->buildUriFromRoute($route, $parameters, $referenceType) . '#'; + + // update info + $data['info'] = 'success'; + } + + return new JsonResponse($data); + } + + /** + * @param ServerRequestInterface $request + * @return array + */ + private function getReferences(ServerRequestInterface $request): array + { + $clipBoard = GeneralUtility::makeInstance(Clipboard::class); + $clipBoard->initializeClipboard($request); + // yes, normal pad can contain only one record, but why not prepare for other pads, too + $clipBoardNormal = $clipBoard->clipData['normal']; + + $references = []; + if (isset($clipBoardNormal['el']) && count($clipBoardNormal['el'])) { + foreach ($clipBoardNormal['el'] as $key => $val) { + $tableAndUid = explode('|', $key); + if ($tableAndUid[0] === 'tt_content') { + $referenceCe = $this->getRecordByUid((int)$tableAndUid[1]); + if ($referenceCe) { + // we don't want Matroschka shortcuts + if ($referenceCe['CType'] === 'shortcut') { + $references[] = $referenceCe['records']; + } else { + $references[] = $tableAndUid[0] . '_' . $tableAndUid[1]; + } + } + } + } + } + + return $references; + } + + /** + * Get the record where the reference + * + * @param int $uid + * @return array + * @throws DBALException + */ + protected function getRecordByUid(int $uid = 0): array + { + $data = []; + + if ($uid) { + $table = 'tt_content'; + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable($table); + // we need hidden ... records, too + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $data = $queryBuilder + ->select('*') + ->from($table) + ->where('uid = :theUid') + ->setParameter('theUid', $uid) + ->execute() + ->fetch(); + } + + return $data; + } + + + /** + * Returns the current BE user. + * + * @return BackendUserAuthentication + */ + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Backend/Preview/ShortcutPreviewRenderer.php b/Classes/Backend/Preview/ShortcutPreviewRenderer.php new file mode 100644 index 0000000..4fc73aa --- /dev/null +++ b/Classes/Backend/Preview/ShortcutPreviewRenderer.php @@ -0,0 +1,24 @@ +' . $content . ''; + } +} diff --git a/Configuration/Backend/AjaxRoutes.php b/Configuration/Backend/AjaxRoutes.php new file mode 100644 index 0000000..a6ca65b --- /dev/null +++ b/Configuration/Backend/AjaxRoutes.php @@ -0,0 +1,7 @@ + [ + 'path' => '/simplereference/get-data', + 'target' => \Ressourcenmangel\Simplereference\Backend\Helper::class . '::getData', + ] +]; diff --git a/Configuration/TCA/Overrides/tt_content.php b/Configuration/TCA/Overrides/tt_content.php new file mode 100644 index 0000000..029ee77 --- /dev/null +++ b/Configuration/TCA/Overrides/tt_content.php @@ -0,0 +1,12 @@ +isFeatureEnabled('fluidBasedPageModule'); + if (false === $fluidBasedPageModule) { + // implement Standard Preview renderer if you need it for older TYPO3 Versions, + // or use Hook in ext_localconf.php; + // $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'] + } else { + $GLOBALS['TCA']['tt_content']['types']['shortcut']['previewRenderer'] = \Ressourcenmangel\Simplereference\Backend\Preview\ShortcutPreviewRenderer::class; + } +}); diff --git a/Resources/Private/Language/de.locallang_modal.xlf b/Resources/Private/Language/de.locallang_modal.xlf new file mode 100644 index 0000000..f28feee --- /dev/null +++ b/Resources/Private/Language/de.locallang_modal.xlf @@ -0,0 +1,20 @@ + + + +
+ + + Referenz einfügen + + + Soll eine Referenz an dieser Position eingefügt werden? + + + Abbrechen + + + Einfügen + + + + diff --git a/Resources/Private/Language/locallang_modal.xlf b/Resources/Private/Language/locallang_modal.xlf new file mode 100644 index 0000000..33af762 --- /dev/null +++ b/Resources/Private/Language/locallang_modal.xlf @@ -0,0 +1,20 @@ + + + +
+ + + Paste Reference + + + Do you want to paste a reference to this position? + + + Cancel + + + Paste + + + + diff --git a/Resources/Public/Backend/Css/styles.css b/Resources/Public/Backend/Css/styles.css new file mode 100644 index 0000000..c4055eb --- /dev/null +++ b/Resources/Public/Backend/Css/styles.css @@ -0,0 +1,6 @@ +.simplereference-shortcut { + display: block; + padding: 10px; + margin: 5px 0 10px; + background-color: rgba(80, 160, 255, 0.25); +} \ No newline at end of file diff --git a/Resources/Public/Icons/Extension.svg b/Resources/Public/Icons/Extension.svg new file mode 100644 index 0000000..79bc11c --- /dev/null +++ b/Resources/Public/Icons/Extension.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/Resources/Public/Icons/simplereference_paste.svg b/Resources/Public/Icons/simplereference_paste.svg new file mode 100644 index 0000000..3b4997c --- /dev/null +++ b/Resources/Public/Icons/simplereference_paste.svg @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/Resources/Public/JavaScript/Main.v10.js b/Resources/Public/JavaScript/Main.v10.js new file mode 100644 index 0000000..f2d94cd --- /dev/null +++ b/Resources/Public/JavaScript/Main.v10.js @@ -0,0 +1,173 @@ +(function () { + // CSS query selector of parent elements with needed data attributes + const _classWrapperColumns = '.t3js-page-column, .t3js-page-ce'; + // CSS query selector of the panel where to insert the add reference button + const _classWrapperAddElement = '.t3js-page-new-ce'; + // CSS query selector which triggers the insert via delegate -> click + const _clicker = '[data-add-reference="1"]'; + // First container with page uid + const _page_infos = document.querySelector('[data-page]'); + // The function object + const SIMPLE_REFERENCE = { + /** + * The needed initial functions, called at the end of this function object + * See last lines here + */ + documentReady: function () { + SIMPLE_REFERENCE.initialRequest({}, false); + SIMPLE_REFERENCE.initializeButtonClick(); + }, + /** + * Add the paste reference button to desired div containers... + */ + addReferenceButtons: function () { + const _contentColumns = document.querySelectorAll(_classWrapperColumns); + + if (_contentColumns.length) { + for (let _contentElement of _contentColumns) { + const _panelAddElement = _contentElement.querySelectorAll(':scope > .t3js-page-ce > ' + _classWrapperAddElement + ',:scope > ' + _classWrapperAddElement); + if (_panelAddElement.length) { + for (let _panel of _panelAddElement) { + if (!_panel.querySelector('[data-add-reference="1"]')) { + const button = SIMPLE_REFERENCE.helper.buttonHtml(_contentElement,_panel); + //_panel.style.background= "blue"; + _panel.insertAdjacentHTML('beforeend', button); + } + } + } + } + } + }, + /** + * Simple delegate click function + */ + initializeButtonClick: function () { + document.addEventListener('click', function (event) { + const isClicker = event.target.matches + ? event.target.matches(_clicker) + : event.target.msMatchesSelector(_clicker); + + if (isClicker && event.target.dataset) { + event.preventDefault(); + SIMPLE_REFERENCE.initialRequest(event.target.dataset, event.target); + } + }, false); + }, + /** + * The initial request to detect if elements are on the clipboard + * @param {object} request A valid object + * @param {object} el A valid object + */ + initialRequest: function (request, el) { + require(['TYPO3/CMS/Core/Ajax/AjaxRequest'], function (AjaxRequest) { + new AjaxRequest(TYPO3.settings.ajaxUrls.simplereference_get_data) + .post(request) + .then( + async function (response) { + const data = await response.resolve(); + if (typeof data === "object" && data !== null && "references" in data) { + SIMPLE_REFERENCE.addReferenceButtons(); + if ("data" in data) { + //SIMPLE_REFERENCE.insertReference(data); + SIMPLE_REFERENCE.openModal(data); + } + } + } + ); + }); + }, + /** + * Calls record_process route + * @param {object} request A valid object with data and optional redirectt + */ + insertReference: function (request) { + require(['TYPO3/CMS/Core/Ajax/AjaxRequest'], function (AjaxRequest) { + new AjaxRequest(TYPO3.settings.ajaxUrls.record_process) + .post(request) + .then( + async function (response) { + const response_data = await response.resolve(); + if (typeof response_data === "object" && "redirect" in response_data) { + location.href = response_data.redirect; + //document.getElementById("#element-tt_content-288").scrollIntoView(); + } + } + ); + }); + }, + /** + * Creates the paste or cancel modal + * @param {object} data A valid object with data and optional redirectt + */ + openModal: function (data) { + require(["TYPO3/CMS/Backend/Modal"], function(Modal) { + const _modalTitle = (TYPO3.lang["simplereference.modal.title"] || "Create Reference"), // + ': "' + this.itemOnClipboardTitle + '"', + _modalText = TYPO3.lang["simplereference.modal.text"] || "Do you want to paste a reference to this position?", + _modalButtons = [{ + text: TYPO3.lang["simplereference.modal.button.cancel"] || "Cancel", + active: true, + btnClass: "btn-default", + trigger: function () { + Modal.currentModal.trigger("modal-dismiss") + } + }, { + text: TYPO3.lang["simplereference.modal.button.paste"] || "Paste", + btnClass: "btn-warning", + trigger: function () { + Modal.currentModal.trigger("modal-dismiss"); + SIMPLE_REFERENCE.insertReference(data); + } + }]; + Modal.show(_modalTitle, _modalText, 1, _modalButtons) + }); + }, + helper: { + /** + * Creates the paste or cancel modal + * To get the page uid you can use here maybe a split on _panel.id, too + * @param {HTMLElement} _contentElement The parent content element or column + * @param {HTMLElement} _panel The panel where the button is added + */ + buttonHtml: function (_contentElement,_panel) { + + if (!_page_infos) { + return ''; + } + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + }, + objToString: function (obj) { + let str = ''; + for (const [p, val] of Object.entries(obj)) { + str += ` data-add-${p}="${val}" `; + } + return str; + } + } + } + // do it + SIMPLE_REFERENCE.documentReady(); +})(); diff --git a/Resources/Public/JavaScript/Main.v11.js b/Resources/Public/JavaScript/Main.v11.js new file mode 100644 index 0000000..89ff70f --- /dev/null +++ b/Resources/Public/JavaScript/Main.v11.js @@ -0,0 +1,177 @@ +(function () { + // CSS query selector of parent elements with needed data attributes + const _classWrapperColumns = '.t3js-page-column, .t3js-page-ce'; + // CSS query selector of the panel where to insert the add reference button + const _classWrapperAddElement = '.t3js-page-new-ce'; + // CSS query selector which triggers the insert via delegate -> click + const _clicker = '[data-add-reference="1"]'; + // First container with page uid + const _page_infos = document.querySelector('[data-page]'); + // The function object + const SIMPLE_REFERENCE = { + /** + * The needed initial functions, called at the end of this function object + * See last lines here + */ + documentReady: function () { + SIMPLE_REFERENCE.initialRequest({}, false); + SIMPLE_REFERENCE.initializeButtonClick(); + }, + /** + * Add the paste reference button to desired div containers... + */ + addReferenceButtons: function () { + const _contentColumns = document.querySelectorAll(_classWrapperColumns); + + if (_contentColumns.length) { + for (let _contentElement of _contentColumns) { + const _panelAddElement = _contentElement.querySelectorAll(':scope > .t3js-page-ce > ' + _classWrapperAddElement + ',:scope > ' + _classWrapperAddElement); + if (_panelAddElement.length) { + for (let _panel of _panelAddElement) { + if (!_panel.querySelector('[data-add-reference="1"]')) { + const button = SIMPLE_REFERENCE.helper.buttonHtml(_contentElement, _panel); + //_panel.style.background= "blue"; + _panel.insertAdjacentHTML('beforeend', button); + } + } + } + } + } + }, + /** + * Simple delegate click function + */ + initializeButtonClick: function () { + document.addEventListener('click', function (event) { + const isClicker = event.target.matches + ? event.target.matches(_clicker) + : event.target.msMatchesSelector(_clicker); + + if (isClicker && event.target.dataset) { + event.preventDefault(); + SIMPLE_REFERENCE.initialRequest(event.target.dataset, event.target); + } + }, false); + }, + /** + * The initial request to detect if elements are on the clipboard + * @param {object} request A valid object + * @param {object} el A valid object + */ + initialRequest: function (request, el) { + require(['TYPO3/CMS/Core/Ajax/AjaxRequest'], function (AjaxRequest) { + new AjaxRequest(TYPO3.settings.ajaxUrls.simplereference_get_data) + .post(request) + .then( + async function (response) { + const data = await response.resolve(); + if (typeof data === "object" && data !== null && "references" in data) { + SIMPLE_REFERENCE.addReferenceButtons(); + if ("data" in data) { + //SIMPLE_REFERENCE.insertReference(data); + SIMPLE_REFERENCE.openModal(data); + } + } + } + ); + }); + }, + /** + * Calls record_process route + * @param {object} request A valid object with data and optional redirectt + */ + insertReference: function (request) { + require(['TYPO3/CMS/Core/Ajax/AjaxRequest'], function (AjaxRequest) { + new AjaxRequest(TYPO3.settings.ajaxUrls.record_process) + .post(request) + .then( + async function (response) { + const response_data = await response.resolve(); + if (typeof response_data === "object" && "redirect" in response_data) { + location.href = response_data.redirect; + //document.getElementById("#element-tt_content-288").scrollIntoView(); + } + } + ); + }); + }, + /** + * Creates the paste or cancel modal + * @param {object} data A valid object with data and optional redirectt + */ + openModal: function (data) { + require(['TYPO3/CMS/Backend/Modal', + 'TYPO3/CMS/Backend/Element/IconElement', + "TYPO3/CMS/Backend/Severity", + "TYPO3/CMS/Backend/Enum/Severity"], function (Modal, Icon, Css, CssEnum) { + + const _modalTitle = (TYPO3.lang["simplereference.modal.title"] || "Create Reference"), // + ': "' + this.itemOnClipboardTitle + '"', + _modalText = TYPO3.lang["simplereference.modal.text"] || "Do you want to paste a reference to this position?", + _modalButtons = [{ + text: TYPO3.lang["simplereference.modal.button.cancel"] || "Cancel", + active: !0, + btnClass: "btn-default", + trigger: function () { + Modal.currentModal.trigger("modal-dismiss") + } + }, { + text: TYPO3.lang["simplereference.modal.button.paste"] || "Paste", + btnClass: "btn-" + Css.getCssClass(CssEnum.SeverityEnum.warning), + trigger: function () { + Modal.currentModal.trigger("modal-dismiss"); + SIMPLE_REFERENCE.insertReference(data); + } + }]; + Modal.show(_modalTitle, _modalText, CssEnum.SeverityEnum.warning, _modalButtons) + }); + }, + helper: { + /** + * Creates the paste or cancel modal + * To get the page uid you can use here maybe a split on _panel.id, too + * @param {HTMLElement} _contentElement The parent content element or column + * @param {HTMLElement} _panel The panel where the button is added + */ + buttonHtml: function (_contentElement,_panel) { + + if (!_page_infos) { + return ''; + } + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + }, + objToString: function (obj) { + let str = ''; + for (const [p, val] of Object.entries(obj)) { + str += ` data-add-${p}="${val}" `; + } + return str; + } + } + } + // do it + SIMPLE_REFERENCE.documentReady(); +})(); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a7f3179 --- /dev/null +++ b/composer.json @@ -0,0 +1,50 @@ +{ + "name": "ressourcenmangel/simplereference", + "type": "typo3-cms-extension", + "description": "Add paste reference button next to paste copy button", + "authors": [ + { + "name": "Matthias Kappenberg", + "email": "matthias.kappenberg@ressourcenmangel.de", + "role": "Developer" + } + ], + "license": "GPL-2.0-or-later", + "require": { + "typo3/cms-core": "^10.4 || ^11.5" + }, + "require-dev": { + "typo3/testing-framework": "^6.8" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "Ressourcenmangel\\Simplereference\\": "Classes" + } + }, + "autoload-dev": { + "psr-4": { + "Ressourcenmangel\\Simplereference\\Tests\\": "Tests" + } + }, + "replace": { + "typo3-ter/simplereference": "self.version" + }, + "config": { + "vendor-dir": ".Build/vendor", + "bin-dir": ".Build/bin" + }, + "scripts": { + "post-autoload-dump": [ + "TYPO3\\TestingFramework\\Composer\\ExtensionTestEnvironment::prepare" + ] + }, + "extra": { + "typo3/cms": { + "cms-package-dir": "{$vendor-dir}/typo3/cms", + "web-dir": ".Build/public", + "extension-key": "simplereference" + } + } +} diff --git a/ext_emconf.php b/ext_emconf.php new file mode 100644 index 0000000..2166b63 --- /dev/null +++ b/ext_emconf.php @@ -0,0 +1,36 @@ + 'Simple Reference', + 'description' => 'Create a simple reference element.', + 'category' => 'plugin', + 'constraints' => + [ + 'depends' => + [ + 'typo3' => '10.4.16-11.5.99', + ], + 'suggests' => + [], + 'conflicts' => + [ + 'gridelements' => '*', + ], + ], + 'autoload' => + [ + 'psr-4' => + [ + 'Ressourcenmangel\\Simplereference\\' => 'Classes', + ], + ], + 'state' => 'stable', + 'uploadfolder' => false, + 'clearCacheOnLoad' => 1, + 'author' => 'Matthias Kappenberg', + 'author_email' => 'matthias.kappenberg@ressourcenmangel.de', + 'author_company' => 'Ressourcenmangel', + 'version' => '1.0.0', + 'clearcacheonload' => true, +]; + diff --git a/ext_localconf.php b/ext_localconf.php new file mode 100644 index 0000000..a580195 --- /dev/null +++ b/ext_localconf.php @@ -0,0 +1,11 @@ +addJsCss'; + } +); + +