From 3ec4a6d526058a1575e1ec98febaf032415fde34 Mon Sep 17 00:00:00 2001 From: "Metzger, Fabian" Date: Wed, 9 Oct 2024 17:06:10 +0200 Subject: [PATCH] [TASK] basic support v13 --- Classes/Form/Element/DataInputElement.php | 89 ++++---- Classes/Service/ExtractorService.php | 15 +- Configuration/JavaScriptModules.php | 12 + Configuration/Services.yaml | 6 + Configuration/TCA/Overrides/tt_content.php | 31 +-- Configuration/page.tsconfig | 5 + Resources/Private/Assets/JavaScript/main.js | 2 +- .../Public/JavaScript/SpreadsheetDataInput.js | 2 +- .../Form/Element/DataInputElementTest.php | 211 +++++------------- ext_localconf.php | 10 - webpack.config.js | 20 +- 11 files changed, 161 insertions(+), 242 deletions(-) create mode 100644 Configuration/JavaScriptModules.php create mode 100644 Configuration/page.tsconfig diff --git a/Classes/Form/Element/DataInputElement.php b/Classes/Form/Element/DataInputElement.php index 9c689c74..defddb5f 100644 --- a/Classes/Form/Element/DataInputElement.php +++ b/Classes/Form/Element/DataInputElement.php @@ -12,9 +12,8 @@ use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Spreadsheet; use TYPO3\CMS\Backend\Form\Element\AbstractFormElement; -use TYPO3\CMS\Backend\Form\NodeFactory; use TYPO3\CMS\Backend\Utility\BackendUtility; -use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction; +use TYPO3\CMS\Core\Page\PageRenderer; use TYPO3\CMS\Core\Resource\FileReference; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Fluid\View\StandaloneView; @@ -23,48 +22,54 @@ class DataInputElement extends AbstractFormElement { private const DEFAULT_TEMPLATE_PATH = 'EXT:spreadsheets/Resources/Private/Templates/FormElement/DataInput.html'; - private readonly ReaderService $readerService; - - private readonly ExtractorService $extractorService; + private ReaderService $readerService; + private ExtractorService $extractorService; + private StandaloneView $view; /** * @var array */ - private array $config; - - private readonly StandaloneView $view; - - /** - * @param array $data - */ - public function __construct(NodeFactory $nodeFactory, array $data) + private array $config = []; + + public function __construct( + ReaderService $readerService, + ExtractorService $extractorService, + StandaloneView $view + ) { + $this->readerService = $readerService; + $this->extractorService = $extractorService; + $this->view = $view; + } + public function setData(array $data): void { - parent::__construct($nodeFactory, $data); - $this->readerService = GeneralUtility::makeInstance(ReaderService::class); - $this->extractorService = GeneralUtility::makeInstance(ExtractorService::class); - $this->config = $this->data['parameterArray']['fieldConf']['config'] ?? []; - - $this->view = GeneralUtility::makeInstance(StandaloneView::class); - $this->view->setTemplatePathAndFilename($this->getTemplatePath()); - $this->view->assign('inputSize', (int)($this->config['size'] ?? 0)); + $this->data = $data; } - /** - * @return array As defined in initializeResultArray() of AbstractNode + * @return array As defined in initializeResultArray() of AbstractFormElement */ public function render(): array { - // get initialize result array from parent abstract node + // Access the $data array + $data = $this->data; + + // Initialize the result array $resultArray = $this->initializeResultArray(); - // upload fields hasn't been specified - if (array_key_exists($this->config['uploadField'], $this->data['processedTca']['columns'] ?? []) === false) { + // Initialize configuration + $this->config = $data['parameterArray']['fieldConf']['config'] ?? []; + + // Set the template path + $this->view->setTemplatePathAndFilename($this->getTemplatePath()); + $this->view->assign('inputSize', (int)($this->config['size'] ?? 0)); + + // Check if upload field is specified + if (!isset($this->config['uploadField']) || !array_key_exists($this->config['uploadField'], $data['processedTca']['columns'] ?? [])) { $resultArray['html'] = $this->view->assign('missingUploadField', true)->render(); return $resultArray; } - // return alert if non valid file references were uploaded + // Get valid file references $references = $this->getValidFileReferences($this->config['uploadField']); if (empty($references)) { $resultArray['html'] = $this->view->assign('nonValidReferences', true)->render(); @@ -72,21 +77,21 @@ public function render(): array return $resultArray; } - // register additional assets only when input will be rendered - $resultArray['requireJsModules'][] = JavaScriptModuleInstruction::forRequireJS( - 'TYPO3/CMS/Spreadsheets/SpreadsheetDataInput' - )->instance($this->data['parameterArray']['itemFormElName'] ?? null); - $resultArray['stylesheetFiles'] = ['EXT:spreadsheets/Resources/Public/Css/SpreadsheetDataInput.css']; + // Register additional assets only when input will be rendered + /** @var PageRenderer $pageRenderer */ + $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); + $pageRenderer->loadJavaScriptModule('@hoogi91/spreadsheets/SpreadsheetDataInput.js'); + $pageRenderer->addCssFile('EXT:spreadsheets/Resources/Public/Css/SpreadsheetDataInput.css'); try { - $valueObject = DsnValueObject::createFromDSN($this->data['parameterArray']['itemFormElValue'] ?? ''); + $valueObject = DsnValueObject::createFromDSN($data['parameterArray']['itemFormElValue'] ?? ''); } catch (InvalidDataSourceNameException) { $valueObject = ''; } $this->view->assignMultiple( [ - 'inputName' => $this->data['parameterArray']['itemFormElName'] ?? null, + 'inputName' => $data['parameterArray']['itemFormElName'] ?? null, 'config' => $this->config, 'sheetFiles' => $references, 'sheetData' => $this->getFileReferencesSpreadsheetData($references), @@ -94,7 +99,7 @@ public function render(): array ] ); - // render view and return result array + // Render view and return result array $resultArray['html'] = $this->view->render(); return $resultArray; @@ -107,7 +112,7 @@ private function getTemplatePath(): string } $templatePath = GeneralUtility::getFileAbsFileName($this->config['template']); - if (is_file($templatePath) === false) { + if (!is_file($templatePath)) { return GeneralUtility::getFileAbsFileName(self::DEFAULT_TEMPLATE_PATH); } @@ -128,7 +133,7 @@ private function getValidFileReferences(string $fieldName): array return []; } - // filter references by allowed types + // Filter references by allowed types return array_filter( $references, static fn ($reference) => in_array($reference->getExtension(), ReaderService::ALLOWED_EXTENSIONS, true) @@ -141,16 +146,16 @@ private function getValidFileReferences(string $fieldName): array */ private function getFileReferencesSpreadsheetData(array $references): array { - // read all spreadsheet from valid file references and filter out invalid references + // Read all spreadsheets from valid file references and filter out invalid references $spreadsheets = $this->getSpreadsheetsByFileReferences($references); - // get data from file references + // Get data from file references $sheetData = []; foreach ($spreadsheets as $fileUid => $spreadsheet) { $sheetData[$fileUid] = $this->getWorksheetDataFromSpreadsheet($spreadsheet); } - // convert whole sheet data content to UTF-8 + // Convert whole sheet data content to UTF-8 array_walk_recursive( $sheetData, static function (&$item): void { @@ -174,7 +179,7 @@ private function getSpreadsheetsByFileReferences(array $references): array try { $spreadsheets[$reference->getUid()] = $this->readerService->getSpreadsheet($reference); } catch (ReaderException) { - // ignore reading non-existing or invalid file reference + // Ignore reading non-existing or invalid file reference } } @@ -195,7 +200,7 @@ private function getWorksheetDataFromSpreadsheet(Spreadsheet $spreadsheet): arra 'cells' => $this->extractorService->rangeToCellArray($worksheet, $worksheetRange), ]; } catch (SpreadsheetException) { - // ignore sheet when an exception occurs + // Ignore sheet when an exception occurs } } diff --git a/Classes/Service/ExtractorService.php b/Classes/Service/ExtractorService.php index 97e39857..a44ca4db 100644 --- a/Classes/Service/ExtractorService.php +++ b/Classes/Service/ExtractorService.php @@ -4,18 +4,19 @@ namespace Hoogi91\Spreadsheets\Service; -use Hoogi91\Spreadsheets\Domain\ValueObject; + use Iterator; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; -use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; -use PhpOffice\PhpSpreadsheet\Reader\Exception as SpreadsheetReaderException; use PhpOffice\PhpSpreadsheet\Worksheet; use PhpOffice\PhpSpreadsheet\Worksheet\Column; use PhpOffice\PhpSpreadsheet\Worksheet\Row; +use Hoogi91\Spreadsheets\Domain\ValueObject; +use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; +use PhpOffice\PhpSpreadsheet\Reader\Exception as SpreadsheetReaderException; use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException; -use TYPO3\CMS\Core\Resource\FileRepository; +use TYPO3\CMS\Core\Resource\ResourceFactory; class ExtractorService { @@ -28,7 +29,7 @@ public function __construct( private readonly SpanService $spanService, private readonly RangeService $rangeService, private readonly ValueMappingService $mappingService, - private readonly FileRepository $fileRepository + private readonly ResourceFactory $resourceFactory ) { } @@ -41,7 +42,7 @@ public function getDataByDsnValueObject( bool $returnCellRef = false ): ValueObject\ExtractionValueObject { $spreadsheet = $this->readerService->getSpreadsheet( - $this->fileRepository->findFileReferenceByUid($dsnValue->getFileReference()) + $this->resourceFactory->getFileReferenceObject($dsnValue->getFileReference()) ); try { @@ -65,7 +66,7 @@ public function getDataByDsnValueObject( ); return ValueObject\ExtractionValueObject::create($spreadsheet, $cellData); - } catch (SpreadsheetException) { + } catch (SpreadsheetException) { return ValueObject\ExtractionValueObject::create($spreadsheet, []); } } diff --git a/Configuration/JavaScriptModules.php b/Configuration/JavaScriptModules.php new file mode 100644 index 00000000..6d73fc64 --- /dev/null +++ b/Configuration/JavaScriptModules.php @@ -0,0 +1,12 @@ + ['backend'], + 'imports' => [ + // recursive definiton, all *.js files in this folder are import-mapped + // trailing slash is required per importmap-specification + '@hoogi91/spreadsheets/' => 'EXT:spreadsheets/Resources/Public/JavaScript/', + ], +]; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 4962d26a..54a6b0e8 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -21,3 +21,9 @@ services: Hoogi91\Spreadsheets\Service\ReaderService: public: true + + Hoogi91\Spreadsheets\Form\Element\DataInputElement: + arguments: + $readerService: '@Hoogi91\Spreadsheets\Service\ReaderService' + $extractorService: '@Hoogi91\Spreadsheets\Service\ExtractorService' + $view: '@TYPO3\CMS\Fluid\View\StandaloneView' diff --git a/Configuration/TCA/Overrides/tt_content.php b/Configuration/TCA/Overrides/tt_content.php index addd8408..65ef3858 100644 --- a/Configuration/TCA/Overrides/tt_content.php +++ b/Configuration/TCA/Overrides/tt_content.php @@ -29,28 +29,29 @@ ); } - // add own assets upload field \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns($table, [ 'tx_spreadsheets_assets' => [ - 'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig( - 'tx_spreadsheets_assets', - [ - 'foreign_table' => 'sys_file_reference', - 'appearance' => [ - 'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/Database.xlf:tt_content.asset_references.addFileReference', - ], - 'overrideChildTca' => [ - 'types' => [ - '0' => [ - 'showitem' => '--palette--;;filePalette', - ], + 'exclude' => 1, + 'label' => 'LLL:EXT:' . $extKey . '/Resources/Private/Language/locallang.xlf:tx_spreadsheets_assets.label', + 'config' => [ + 'type' => 'file', + 'appearance' => [ + 'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/Database.xlf:tt_content.asset_references.addFileReference', + ], + 'overrideChildTca' => [ + 'types' => [ + \TYPO3\CMS\Core\Resource\File::FILETYPE_APPLICATION => [ + 'showitem' => '--palette--;;filePalette', ], ], ], - implode(',', \Hoogi91\Spreadsheets\Service\ReaderService::ALLOWED_EXTENSIONS) - ), + 'allowed' => implode(',', \Hoogi91\Spreadsheets\Service\ReaderService::ALLOWED_EXTENSIONS), + 'maxitems' => 1, // Adjust this based on how many items can be uploaded + ], ], 'tx_spreadsheets_ignore_styles' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:' . $extKey . '/Resources/Private/Language/locallang.xlf:tca.tx_spreadsheets_ignore_styles.label', 'config' => [ 'type' => 'check', 'items' => [ diff --git a/Configuration/page.tsconfig b/Configuration/page.tsconfig new file mode 100644 index 00000000..04706e3a --- /dev/null +++ b/Configuration/page.tsconfig @@ -0,0 +1,5 @@ + +// add content element to insert tables in content element wizard +// register template for backend preview rendering +@import 'EXT:spreadsheets/Configuration/PageTSconfig/NewContentElementWizard.typoscript' +@import 'EXT:spreadsheets/Configuration/PageTSconfig/BackendPreview.typoscript' diff --git a/Resources/Private/Assets/JavaScript/main.js b/Resources/Private/Assets/JavaScript/main.js index 76481ece..2f336b26 100644 --- a/Resources/Private/Assets/JavaScript/main.js +++ b/Resources/Private/Assets/JavaScript/main.js @@ -2,7 +2,7 @@ import DSN from './dsn.js'; import Renderer from './renderer.js'; import Spreadsheet from './spreadsheet.js'; import Selector from "./selector.js"; -import DocumentService from 'DocumentService'; +import DocumentService from '@typo3/core/document-service.js'; class SpreadsheetDataInput { constructor(element) { diff --git a/Resources/Public/JavaScript/SpreadsheetDataInput.js b/Resources/Public/JavaScript/SpreadsheetDataInput.js index 3b69c323..86d550b3 100644 --- a/Resources/Public/JavaScript/SpreadsheetDataInput.js +++ b/Resources/Public/JavaScript/SpreadsheetDataInput.js @@ -1 +1 @@ -define(["TYPO3/CMS/Core/DocumentService"],(e=>(()=>{"use strict";var t={722:t=>{t.exports=e}},s={};function i(e){const t=e.toString(24);let s="";for(let e=0;e2&&void 0!==arguments[2]?arguments[2]:null;return"row"===s?t:"column"===s?i(e):i(e)+t}function l(e){let t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],s=parseInt(e.getAttribute("data-col"));!0===t&&!0===e.hasAttribute("colspan")&&(s+=parseInt(e.getAttribute("colspan"))-1);let i=parseInt(e.getAttribute("data-row"));return!0===t&&!0===e.hasAttribute("rowspan")&&(i+=parseInt(e.getAttribute("rowspan"))-1),{colIndex:s,rowIndex:i}}class a{constructor(e){if(this.properties={},0===e.length)return;const t=e.match(/^spreadsheet:\/\/(\d+)(?:\?(.+))?/);if(null===t)throw new Error('DSN class expects value to be of type string and format "spreadsheet://index=0..."');if(void 0===t[2])this.properties.fileUid=t[1];else{const e=JSON.parse('{"'+t[2].replace(/&/g,'","').replace(/=/g,'":"')+'"}',(function(e,t){return""===e?t:decodeURIComponent(t)}));this.properties.fileUid=t[1],this.properties.index=e.index||0,this.properties.direction=e.direction||"horizontal",this.range=e.range||""}}get fileUid(){return this.properties.fileUid}set fileUid(e){this.properties.fileUid=e}get index(){return this.properties.index}set index(e){this.properties.index=e}get coordinates(){return this.properties.coordinates||null}get range(){return this.properties.range||""}set range(e){this.properties.range=e;let t=e.match(/^([A-Z]+|\d+)(\d+)?:([A-Z]+|\d+)(\d+)?$/);null!==t&&(t=Array.from(t).slice(1),Number.isNaN(parseInt(t[0]))||(t[1]=parseInt(t[0]),t[0]=null),Number.isNaN(parseInt(t[2]))||(t[3]=parseInt(t[2]),t[2]=null),t[0]=t[0]||t[2]||null,t[2]=t[2]||t[0],t[1]=t[1]||t[3]||null,t[3]=t[3]||t[1],this.properties.coordinates={startCol:null!==t[0]?r(t[0]):null,startRow:null!==t[1]?parseInt(t[1]):null,endCol:null!==t[2]?r(t[2]):null,endRow:null!==t[3]?parseInt(t[3]):null})}get direction(){return this.properties.direction||""}set direction(e){this.properties.direction=e}}class o{constructor(e,t){this.sheetWrapper=e,this.sheetWrapper.addEventListener("click",(e=>{if("A"===e.target.tagName){for(let t of e.target.parentNode.childNodes)t.classList.remove("active");e.target.classList.add("active"),this.sheetWrapper.dispatchEvent(new CustomEvent("changeIndex",{detail:{index:e.target.getAttribute("data-value")}}))}})),null!==t&&(this.tableWrapper=t)}update(e,t){if(!(t instanceof a))throw new Error('Renderer class "update" method expects parameter to be type of a DSN class');this.buildTabs(e,t.index),this.buildTable(e,t.coordinates)}buildTabs(e,t){if(this.sheetWrapper.textContent="",e.getAllSheets().length<=0)this.sheetWrapper.style.display="none";else{for(let s=0;s1&&void 0!==arguments[1]?arguments[1]:null;if(void 0===this.tableWrapper||null===this.tableWrapper)return;const s=Object.values(e.getSheetData()).map((e=>Object.values(e)));if(s.length<=0)return;const i=document.createElement("table");this.buildTableHeader(i,Math.max(...s.map((e=>e.length)))),this.buildTableBody(i,s,t),this.tableWrapper.textContent="",this.tableWrapper.appendChild(i),this.tableWrapper.style.display="block"}buildTableHeader(e,t){const s=e.createTHead().insertRow();for(let e=0;e<=t;e++)if(e>0){const t=s.insertCell();t.innerText=i(e),t.setAttribute("data-col",e)}else s.insertCell()}buildTableBody(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const i=e.createTBody();let r=[];t.forEach(((e,t)=>{const n=i.insertRow(),l=n.insertCell();l.innerText=t+1,l.setAttribute("data-row",t+1);let a=0;e.forEach((e=>{const i=n.insertCell();if(i.innerText=e.val,void 0!==e.css&&i.setAttribute("class",e.css.split("-").filter((e=>e.length>0)).map((e=>"align-"+e)).join(" ")),void 0!==e.col){i.setAttribute("colspan",e.col);for(let s=1;s=t+1&&s.startCol<=a+1&&s.endCol>=a+1&&i.classList.add("highlight"),a++}))}))}}class d{constructor(e,t){if(!(e instanceof a))throw new Error("Spreadsheet class expects dsn parameter to be type of a DSN class");this.data=t,this.defaultFileUid=e.fileUid,this.defaultSheetIndex=e.index}set dsn(e){if(!(e instanceof a))throw new Error('Spreadsheet class setter "dsn" expects parameter to be type of a DSN class');this.defaultFileUid=e.fileUid,this.defaultSheetIndex=e.index}getAllSheets(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.defaultFileUid;return this.data[e]||[]}getSheet(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.defaultSheetIndex,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.defaultFileUid;return void 0===this.data[t]?[]:this.data[t][e]||[]}getSheetName(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.defaultSheetIndex,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.defaultFileUid;return this.getSheet(e,t).name||""}getSheetData(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.defaultSheetIndex,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.defaultFileUid;return this.getSheet(e,t).cells||[]}}class h{constructor(e){if(this.cursor={isSelecting:!1,selectMode:null},this.properties={},this.tableWrapper=e,null!==this.tableWrapper){this.tableWrapper.addEventListener("mousedown",(e=>{const t=document.elementFromPoint(e.x,e.y);this.cursor.isSelecting=!0,this.cursor.start=t,this.cursor.selectMode=null,this.reachedColumnHeader(t)?this.cursor.selectMode="column":this.reachedRowHeader(t)&&(this.cursor.selectMode="row")}));const e=e=>{if(!0!==this.cursor.isSelecting)return!1;"mouseup"===e.type&&(this.cursor.isSelecting=!1,window.getSelection?window.getSelection().empty?window.getSelection().empty():window.getSelection().removeAllRanges&&window.getSelection().removeAllRanges():document.selection&&document.selection.empty());const t=document.elementFromPoint(e.x,e.y);return!1!==this.isInsideTable(t)&&((!this.reachedColumnHeader(t)||!this.reachedRowHeader(t))&&(("column"!==this.cursor.selectMode||!this.reachedRowHeader(t))&&(("row"!==this.cursor.selectMode||!this.reachedColumnHeader(t))&&((null!==this.cursor.selectMode||!this.reachedColumnHeader(t)&&!this.reachedRowHeader(t))&&(t!==this.cursor.start?(this.cursor.end=t,this.selection=[this.cursor.start,this.cursor.end]):this.selection=[this.cursor.start],this.calculateMergeCells(),this.highlightSelection(),void this.tableWrapper.dispatchEvent(new CustomEvent("changeSelection",{detail:{start:this.selection.start,end:this.selection.end}})))))))};this.tableWrapper.addEventListener("mousemove",function(e,t){let s,i;return function(){const r=this,n=arguments;i?(clearTimeout(s),s=setTimeout((function(){Date.now()-i>=e&&(t.apply(r,n),i=Date.now())}),e-(Date.now()-i))):(t.apply(r,n),i=Date.now())}}(60,e)),this.tableWrapper.addEventListener("mouseup",e)}}get selection(){return this.properties.selection}set selection(e){if(e.length<=0)return;let t={min:null,max:null},s={min:null,max:null};e.forEach((e=>{const i=l(e,!1);(null===t.min||t.min>i.colIndex)&&(t.min=i.colIndex),(null===s.min||s.min>i.rowIndex)&&(s.min=i.rowIndex);const r=l(e,!0);(null===t.max||t.max0&&void 0!==arguments[0]?arguments[0]:[],t=this.selection.indexes.col,s=this.selection.indexes.row;"row"===this.cursor.selectMode?t={min:1,max:this.tableWrapper.querySelector("table").rows[0].cells.length}:"column"===this.cursor.selectMode&&(s={min:1,max:this.tableWrapper.querySelector("table").rows.length});e:for(let i=s.min;i<=s.max;i++)for(let r=t.min;r<=t.max;r++){if(-1!==e.indexOf(r+"-"+i))continue;const n=this.tableWrapper.querySelector('td[data-col="'+r+'"][data-row="'+i+'"][colspan],td[data-col="'+r+'"][data-row="'+i+'"][rowspan]');if(null===n)continue;const a=l(n,!1);e.push(a.colIndex+"-"+a.rowIndex);const o=l(n,!0);if(o.colIndex>t.max||o.rowIndex>s.max){this.selection=[...this.selection.elements,n],this.calculateMergeCells(e);break e}}const i=this.tableWrapper.querySelectorAll("td[colspan], td[rowspan]");for(let r=0;r=t.min&&o.rowIndex>=s.min){this.selection=[...this.selection.elements,n],this.calculateMergeCells(e);break}}}highlightSelection(){const e=this.selection.indexes.col,t=this.selection.indexes.row,s=[];if("row"===this.cursor.selectMode)for(let e=t.min;e<=t.max;e++)s.push(...this.tableWrapper.querySelectorAll('td[data-row="'+e+'"]'));else if("column"===this.cursor.selectMode)for(let t=e.min;t<=e.max;t++)s.push(...this.tableWrapper.querySelectorAll('td[data-col="'+t+'"]'));else for(let i=e.min;i<=e.max;i++)for(let e=t.min;e<=t.max;e++)s.push(this.tableWrapper.querySelector('td[data-col="'+i+'"][data-row="'+e+'"]'));Array.from(this.tableWrapper.querySelectorAll("td.highlight")).filter((e=>null!==e)).forEach((e=>e.classList.remove("highlight"))),s.filter((e=>null!==e)).forEach((e=>e.classList.add("highlight")))}}var c=function e(i){var r=s[i];if(void 0!==r)return r.exports;var n=s[i]={exports:{}};return t[i](n,n.exports,e),n.exports}(722);class p{constructor(e){this.element=e,this.sheetWrapper=this.element.querySelector(".spreadsheet-sheets"),this.tableWrapper=this.element.querySelector(".spreadsheet-table"),this.fileInput=this.element.querySelector(".spreadsheet-file-select"),this.directionInput=this.element.querySelector(".spreadsheet-input-direction"),this.resetInput=this.element.querySelector(".spreadsheet-reset-button"),this.unsetInput=this.element.querySelector(".spreadsheet-unset-button"),this.originalDataInput=this.element.querySelector("input.spreadsheet-input-original"),this.databaseDataInput=this.element.querySelector("input.spreadsheet-input-database"),this.formattedDataInput=this.element.querySelector("input.spreadsheet-input-formatted"),this.dsn=new a(this.databaseDataInput.getAttribute("value")),this.spreadsheet=new d(this.dsn,JSON.parse(this.element.getAttribute("data-spreadsheet"))),this.renderer=new o(this.sheetWrapper,this.tableWrapper),this.selector=new h(this.tableWrapper),this.updateSpreadsheet(!0),this.initializeEvents()}initializeEvents(){this.fileInput.addEventListener("change",(e=>{this.dsn.fileUid=e.currentTarget.value,this.dsn.index=0,this.dsn.range="",this.updateSpreadsheet(!0)})),this.sheetWrapper.addEventListener("changeIndex",(e=>{this.dsn.index=e.detail.index,this.updateSpreadsheet(!0)})),this.resetInput.addEventListener("click",(()=>{this.dsn=new a(this.originalDataInput.getAttribute("value")),this.updateSpreadsheet(!0)})),this.unsetInput.addEventListener("click",(()=>{this.dsn=new a(""),this.sheetWrapper.style.display="none",null!==this.tableWrapper&&(this.tableWrapper.style.display="none"),null!==this.directionInput&&(this.directionInput.disabled=!0),this.updateSpreadsheet()})),null!==this.tableWrapper&&this.tableWrapper.addEventListener("changeSelection",(e=>{"string"==typeof e.detail.start&&e.detail.start===e.detail.end&&e.detail.start.match(/^(?=.*\d)(?=.*[A-Z]).+$/)?this.dsn.range=e.detail.start:this.dsn.range=e.detail.start+":"+e.detail.end,this.updateSpreadsheet()})),null!==this.tableWrapper&&null!==this.directionInput&&this.directionInput.addEventListener("click",(()=>{this.dsn.direction="horizontal"===(this.dsn.direction||"horizontal")?"vertical":"horizontal",this.updateSpreadsheet()}))}updateSpreadsheet(){let e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.spreadsheet.dsn=this.dsn,!0===e&&this.renderer.update(this.spreadsheet,this.dsn),this.fileInput.value=this.dsn.fileUid,null!==this.directionInput&&("vertical"===this.dsn.direction?(this.directionInput.querySelector(".direction-row").style.display="none",this.directionInput.querySelector(".direction-column").style.display="block"):(this.directionInput.querySelector(".direction-column").style.display="none",this.directionInput.querySelector(".direction-row").style.display="block"));let t=this.spreadsheet.getSheetName(),s="";void 0!==this.dsn.fileUid&&void 0!==this.dsn.index&&(s+="spreadsheet://"+this.dsn.fileUid+"?index="+this.dsn.index),null!==this.tableWrapper&&this.dsn.range.length>0&&(t+=" - "+this.dsn.range,s+="&range="+this.dsn.range),null!==this.tableWrapper&&null!==this.directionInput&&this.dsn.direction.length>0&&(s+="&direction="+this.dsn.direction),this.formattedDataInput.setAttribute("value",t),this.databaseDataInput.setAttribute("value",s),""!==s&&(this.sheetWrapper.style.display="",null!==this.tableWrapper&&(this.tableWrapper.style.display=""),null!==this.directionInput&&(this.directionInput.disabled=!1))}}return c.ready().then((()=>{document.querySelectorAll(".spreadsheet-input-wrap").forEach((e=>{new p(e)}))})).catch((()=>{console.error("Failed to load DOM for processing spreadsheet inputs!")})),{}})())); \ No newline at end of file +import*as e from"@typo3/backend/document-service.js";var t={};function s(e){const t=e.toString(24);let s="";for(let e=0;e2&&void 0!==arguments[2]?arguments[2]:null;return"row"===i?t:"column"===i?s(e):s(e)+t}function n(e){let t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],s=parseInt(e.getAttribute("data-col"));!0===t&&!0===e.hasAttribute("colspan")&&(s+=parseInt(e.getAttribute("colspan"))-1);let i=parseInt(e.getAttribute("data-row"));return!0===t&&!0===e.hasAttribute("rowspan")&&(i+=parseInt(e.getAttribute("rowspan"))-1),{colIndex:s,rowIndex:i}}t.d=(e,s)=>{for(var i in s)t.o(s,i)&&!t.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:s[i]})},t.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);class l{constructor(e){if(this.properties={},0===e.length)return;const t=e.match(/^spreadsheet:\/\/(\d+)(?:\?(.+))?/);if(null===t)throw new Error('DSN class expects value to be of type string and format "spreadsheet://index=0..."');if(void 0===t[2])this.properties.fileUid=t[1];else{const e=JSON.parse('{"'+t[2].replace(/&/g,'","').replace(/=/g,'":"')+'"}',(function(e,t){return""===e?t:decodeURIComponent(t)}));this.properties.fileUid=t[1],this.properties.index=e.index||0,this.properties.direction=e.direction||"horizontal",this.range=e.range||""}}get fileUid(){return this.properties.fileUid}set fileUid(e){this.properties.fileUid=e}get index(){return this.properties.index}set index(e){this.properties.index=e}get coordinates(){return this.properties.coordinates||null}get range(){return this.properties.range||""}set range(e){this.properties.range=e;let t=e.match(/^([A-Z]+|\d+)(\d+)?:([A-Z]+|\d+)(\d+)?$/);null!==t&&(t=Array.from(t).slice(1),Number.isNaN(parseInt(t[0]))||(t[1]=parseInt(t[0]),t[0]=null),Number.isNaN(parseInt(t[2]))||(t[3]=parseInt(t[2]),t[2]=null),t[0]=t[0]||t[2]||null,t[2]=t[2]||t[0],t[1]=t[1]||t[3]||null,t[3]=t[3]||t[1],this.properties.coordinates={startCol:null!==t[0]?i(t[0]):null,startRow:null!==t[1]?parseInt(t[1]):null,endCol:null!==t[2]?i(t[2]):null,endRow:null!==t[3]?parseInt(t[3]):null})}get direction(){return this.properties.direction||""}set direction(e){this.properties.direction=e}}class a{constructor(e,t){this.sheetWrapper=e,this.sheetWrapper.addEventListener("click",(e=>{if("A"===e.target.tagName){for(let t of e.target.parentNode.childNodes)t.classList.remove("active");e.target.classList.add("active"),this.sheetWrapper.dispatchEvent(new CustomEvent("changeIndex",{detail:{index:e.target.getAttribute("data-value")}}))}})),null!==t&&(this.tableWrapper=t)}update(e,t){if(!(t instanceof l))throw new Error('Renderer class "update" method expects parameter to be type of a DSN class');this.buildTabs(e,t.index),this.buildTable(e,t.coordinates)}buildTabs(e,t){if(this.sheetWrapper.textContent="",e.getAllSheets().length<=0)this.sheetWrapper.style.display="none";else{for(let s=0;s1&&void 0!==arguments[1]?arguments[1]:null;if(void 0===this.tableWrapper||null===this.tableWrapper)return;const s=Object.values(e.getSheetData()).map((e=>Object.values(e)));if(s.length<=0)return;const i=document.createElement("table");this.buildTableHeader(i,Math.max(...s.map((e=>e.length)))),this.buildTableBody(i,s,t),this.tableWrapper.textContent="",this.tableWrapper.appendChild(i),this.tableWrapper.style.display="block"}buildTableHeader(e,t){const i=e.createTHead().insertRow();for(let e=0;e<=t;e++)if(e>0){const t=i.insertCell();t.innerText=s(e),t.setAttribute("data-col",e)}else i.insertCell()}buildTableBody(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const i=e.createTBody();let r=[];t.forEach(((e,t)=>{const n=i.insertRow(),l=n.insertCell();l.innerText=t+1,l.setAttribute("data-row",t+1);let a=0;e.forEach((e=>{const i=n.insertCell();if(i.innerText=e.val,void 0!==e.css&&i.setAttribute("class",e.css.split("-").filter((e=>e.length>0)).map((e=>"align-"+e)).join(" ")),void 0!==e.col){i.setAttribute("colspan",e.col);for(let s=1;s=t+1&&s.startCol<=a+1&&s.endCol>=a+1&&i.classList.add("highlight"),a++}))}))}}class o{constructor(e,t){if(!(e instanceof l))throw new Error("Spreadsheet class expects dsn parameter to be type of a DSN class");this.data=t,this.defaultFileUid=e.fileUid,this.defaultSheetIndex=e.index}set dsn(e){if(!(e instanceof l))throw new Error('Spreadsheet class setter "dsn" expects parameter to be type of a DSN class');this.defaultFileUid=e.fileUid,this.defaultSheetIndex=e.index}getAllSheets(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.defaultFileUid;return this.data[e]||[]}getSheet(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.defaultSheetIndex,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.defaultFileUid;return void 0===this.data[t]?[]:this.data[t][e]||[]}getSheetName(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.defaultSheetIndex,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.defaultFileUid;return this.getSheet(e,t).name||""}getSheetData(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.defaultSheetIndex,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.defaultFileUid;return this.getSheet(e,t).cells||[]}}class d{constructor(e){if(this.cursor={isSelecting:!1,selectMode:null},this.properties={},this.tableWrapper=e,null!==this.tableWrapper){this.tableWrapper.addEventListener("mousedown",(e=>{const t=document.elementFromPoint(e.x,e.y);this.cursor.isSelecting=!0,this.cursor.start=t,this.cursor.selectMode=null,this.reachedColumnHeader(t)?this.cursor.selectMode="column":this.reachedRowHeader(t)&&(this.cursor.selectMode="row")}));const e=e=>{if(!0!==this.cursor.isSelecting)return!1;"mouseup"===e.type&&(this.cursor.isSelecting=!1,window.getSelection?window.getSelection().empty?window.getSelection().empty():window.getSelection().removeAllRanges&&window.getSelection().removeAllRanges():document.selection&&document.selection.empty());const t=document.elementFromPoint(e.x,e.y);return!1!==this.isInsideTable(t)&&((!this.reachedColumnHeader(t)||!this.reachedRowHeader(t))&&(("column"!==this.cursor.selectMode||!this.reachedRowHeader(t))&&(("row"!==this.cursor.selectMode||!this.reachedColumnHeader(t))&&((null!==this.cursor.selectMode||!this.reachedColumnHeader(t)&&!this.reachedRowHeader(t))&&(t!==this.cursor.start?(this.cursor.end=t,this.selection=[this.cursor.start,this.cursor.end]):this.selection=[this.cursor.start],this.calculateMergeCells(),this.highlightSelection(),void this.tableWrapper.dispatchEvent(new CustomEvent("changeSelection",{detail:{start:this.selection.start,end:this.selection.end}})))))))};this.tableWrapper.addEventListener("mousemove",function(e,t){let s,i;return function(){const r=this,n=arguments;i?(clearTimeout(s),s=setTimeout((function(){Date.now()-i>=e&&(t.apply(r,n),i=Date.now())}),e-(Date.now()-i))):(t.apply(r,n),i=Date.now())}}(60,e)),this.tableWrapper.addEventListener("mouseup",e)}}get selection(){return this.properties.selection}set selection(e){if(e.length<=0)return;let t={min:null,max:null},s={min:null,max:null};e.forEach((e=>{const i=n(e,!1);(null===t.min||t.min>i.colIndex)&&(t.min=i.colIndex),(null===s.min||s.min>i.rowIndex)&&(s.min=i.rowIndex);const r=n(e,!0);(null===t.max||t.max0&&void 0!==arguments[0]?arguments[0]:[],t=this.selection.indexes.col,s=this.selection.indexes.row;"row"===this.cursor.selectMode?t={min:1,max:this.tableWrapper.querySelector("table").rows[0].cells.length}:"column"===this.cursor.selectMode&&(s={min:1,max:this.tableWrapper.querySelector("table").rows.length});e:for(let i=s.min;i<=s.max;i++)for(let r=t.min;r<=t.max;r++){if(-1!==e.indexOf(r+"-"+i))continue;const l=this.tableWrapper.querySelector('td[data-col="'+r+'"][data-row="'+i+'"][colspan],td[data-col="'+r+'"][data-row="'+i+'"][rowspan]');if(null===l)continue;const a=n(l,!1);e.push(a.colIndex+"-"+a.rowIndex);const o=n(l,!0);if(o.colIndex>t.max||o.rowIndex>s.max){this.selection=[...this.selection.elements,l],this.calculateMergeCells(e);break e}}const i=this.tableWrapper.querySelectorAll("td[colspan], td[rowspan]");for(let r=0;r=t.min&&o.rowIndex>=s.min){this.selection=[...this.selection.elements,l],this.calculateMergeCells(e);break}}}highlightSelection(){const e=this.selection.indexes.col,t=this.selection.indexes.row,s=[];if("row"===this.cursor.selectMode)for(let e=t.min;e<=t.max;e++)s.push(...this.tableWrapper.querySelectorAll('td[data-row="'+e+'"]'));else if("column"===this.cursor.selectMode)for(let t=e.min;t<=e.max;t++)s.push(...this.tableWrapper.querySelectorAll('td[data-col="'+t+'"]'));else for(let i=e.min;i<=e.max;i++)for(let e=t.min;e<=t.max;e++)s.push(this.tableWrapper.querySelector('td[data-col="'+i+'"][data-row="'+e+'"]'));Array.from(this.tableWrapper.querySelectorAll("td.highlight")).filter((e=>null!==e)).forEach((e=>e.classList.remove("highlight"))),s.filter((e=>null!==e)).forEach((e=>e.classList.add("highlight")))}}const h=(e=>{var s={};return t.d(s,e),s})({default:()=>e.default});class c{constructor(e){this.element=e,this.sheetWrapper=this.element.querySelector(".spreadsheet-sheets"),this.tableWrapper=this.element.querySelector(".spreadsheet-table"),this.fileInput=this.element.querySelector(".spreadsheet-file-select"),this.directionInput=this.element.querySelector(".spreadsheet-input-direction"),this.resetInput=this.element.querySelector(".spreadsheet-reset-button"),this.unsetInput=this.element.querySelector(".spreadsheet-unset-button"),this.originalDataInput=this.element.querySelector("input.spreadsheet-input-original"),this.databaseDataInput=this.element.querySelector("input.spreadsheet-input-database"),this.formattedDataInput=this.element.querySelector("input.spreadsheet-input-formatted"),this.dsn=new l(this.databaseDataInput.getAttribute("value")),this.spreadsheet=new o(this.dsn,JSON.parse(this.element.getAttribute("data-spreadsheet"))),this.renderer=new a(this.sheetWrapper,this.tableWrapper),this.selector=new d(this.tableWrapper),this.updateSpreadsheet(!0),this.initializeEvents()}initializeEvents(){this.fileInput.addEventListener("change",(e=>{this.dsn.fileUid=e.currentTarget.value,this.dsn.index=0,this.dsn.range="",this.updateSpreadsheet(!0)})),this.sheetWrapper.addEventListener("changeIndex",(e=>{this.dsn.index=e.detail.index,this.updateSpreadsheet(!0)})),this.resetInput.addEventListener("click",(()=>{this.dsn=new l(this.originalDataInput.getAttribute("value")),this.updateSpreadsheet(!0)})),this.unsetInput.addEventListener("click",(()=>{this.dsn=new l(""),this.sheetWrapper.style.display="none",null!==this.tableWrapper&&(this.tableWrapper.style.display="none"),null!==this.directionInput&&(this.directionInput.disabled=!0),this.updateSpreadsheet()})),null!==this.tableWrapper&&this.tableWrapper.addEventListener("changeSelection",(e=>{"string"==typeof e.detail.start&&e.detail.start===e.detail.end&&e.detail.start.match(/^(?=.*\d)(?=.*[A-Z]).+$/)?this.dsn.range=e.detail.start:this.dsn.range=e.detail.start+":"+e.detail.end,this.updateSpreadsheet()})),null!==this.tableWrapper&&null!==this.directionInput&&this.directionInput.addEventListener("click",(()=>{this.dsn.direction="horizontal"===(this.dsn.direction||"horizontal")?"vertical":"horizontal",this.updateSpreadsheet()}))}updateSpreadsheet(){let e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.spreadsheet.dsn=this.dsn,!0===e&&this.renderer.update(this.spreadsheet,this.dsn),this.fileInput.value=this.dsn.fileUid,null!==this.directionInput&&("vertical"===this.dsn.direction?(this.directionInput.querySelector(".direction-row").style.display="none",this.directionInput.querySelector(".direction-column").style.display="block"):(this.directionInput.querySelector(".direction-column").style.display="none",this.directionInput.querySelector(".direction-row").style.display="block"));let t=this.spreadsheet.getSheetName(),s="";void 0!==this.dsn.fileUid&&void 0!==this.dsn.index&&(s+="spreadsheet://"+this.dsn.fileUid+"?index="+this.dsn.index),null!==this.tableWrapper&&this.dsn.range.length>0&&(t+=" - "+this.dsn.range,s+="&range="+this.dsn.range),null!==this.tableWrapper&&null!==this.directionInput&&this.dsn.direction.length>0&&(s+="&direction="+this.dsn.direction),this.formattedDataInput.setAttribute("value",t),this.databaseDataInput.setAttribute("value",s),""!==s&&(this.sheetWrapper.style.display="",null!==this.tableWrapper&&(this.tableWrapper.style.display=""),null!==this.directionInput&&(this.directionInput.disabled=!1))}}h.default.ready().then((()=>{document.querySelectorAll(".spreadsheet-input-wrap").forEach((e=>{new c(e)}))})).catch((()=>{console.error("Failed to load DOM for processing spreadsheet inputs!")})); \ No newline at end of file diff --git a/Tests/Unit/Form/Element/DataInputElementTest.php b/Tests/Unit/Form/Element/DataInputElementTest.php index 72b9d0d4..2bcec2df 100644 --- a/Tests/Unit/Form/Element/DataInputElementTest.php +++ b/Tests/Unit/Form/Element/DataInputElementTest.php @@ -14,10 +14,9 @@ use PHPUnit\Framework\MockObject\MockObject; use Traversable; use TYPO3\CMS\Backend\Form\NodeFactory; -use TYPO3\CMS\Core\Database\RelationHandler; use TYPO3\CMS\Core\Imaging\IconFactory; use TYPO3\CMS\Core\Information\Typo3Version; -use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction; +use TYPO3\CMS\Core\Page\PageRenderer; use TYPO3\CMS\Core\Resource\FileReference; use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -37,7 +36,7 @@ class DataInputElementTest extends UnitTestCase private const FILE_REFERENCE_TYPE_MAP = [ 0 => 'null', - // this file does not exists + // this file does not exist 465 => 'xlsx', // should work 589 => 'pdf', @@ -69,7 +68,7 @@ class DataInputElementTest extends UnitTestCase 'additionalHiddenFields' => [], 'additionalInlineLanguageLabelFiles' => [], 'stylesheetFiles' => [], - 'requireJsModules' => [], + 'javaScriptModules' => [], 'inlineData' => [], ]; @@ -94,17 +93,11 @@ class DataInputElementTest extends UnitTestCase 'valueObject' => 'spreadsheet://465?index=1&range=D2%3AG5&direction=vertical', ]; - private ReaderService&MockObject $readerService; - - private ExtractorService&MockObject $extractorService; - - private MockObject&StandaloneView $standaloneView; - - private MockObject&IconFactory $iconFactory; - - private MockObject&RelationHandler $relationHandler; - - private MockObject&ResourceFactory $resourceFactory; + private ReaderService $readerService; + private ExtractorService $extractorService; + private StandaloneView $standaloneView; + private IconFactory $iconFactory; + private ResourceFactory $resourceFactory; /** * @var array @@ -115,47 +108,45 @@ class DataInputElementTest extends UnitTestCase protected function setUp(): void { - $trueCallback = static fn (callable $callback) => self::callback( - static fn () => call_user_func_array($callback, func_get_args()) !== false - ); - parent::setUp(); - $spreadsheet = (new Xlsx())->load(dirname(__DIR__, 3) . '/Fixtures/01_fixture.xlsx'); + + // Mock services $this->readerService = $this->createMock(ReaderService::class); - $this->readerService->method('getSpreadsheet')->willReturn($spreadsheet); - $this->standaloneView = $this->createMock(StandaloneView::class); - $this->standaloneView->method('assign')->with( - $trueCallback(static fn ($key) => self::$assignedVariables['_next'] = $key), - $trueCallback(static function ($value): void { - self::$assignedVariables[self::$assignedVariables['_next']] = $value; - unset(self::$assignedVariables['_next']); - }) - )->willReturnSelf(); - $this->standaloneView->method('assignMultiple')->with($trueCallback( - static fn ($values) => self::$assignedVariables = array_merge(self::$assignedVariables, $values) - ))->willReturnSelf(); - $this->standaloneView->method('render')->willReturnCallback(static fn () => self::$assignedVariables); $this->extractorService = $this->createMock(ExtractorService::class); - $this->extractorService->method('rangeToCellArray')->willReturn([ - 'A1' => file_get_contents(dirname(__DIR__, 3) . '/Fixtures/latin1-content.txt'), - ]); + $this->standaloneView = $this->createMock(StandaloneView::class); $this->iconFactory = $this->createMock(IconFactory::class); + $this->resourceFactory = $this->createMock(ResourceFactory::class); + // Register mocks GeneralUtility::addInstance(ReaderService::class, $this->readerService); GeneralUtility::addInstance(ExtractorService::class, $this->extractorService); GeneralUtility::addInstance(StandaloneView::class, $this->standaloneView); GeneralUtility::addInstance(IconFactory::class, $this->iconFactory); + GeneralUtility::addInstance(ResourceFactory::class, $this->resourceFactory); - // mock file reference handler to get valid files - $this->relationHandler = $this->createMock(RelationHandler::class); - $this->resourceFactory = $this->createMock(ResourceFactory::class); + // Setup expectations for StandaloneView + $this->standaloneView->method('assign')->willReturnSelf(); + $this->standaloneView->method('assignMultiple')->willReturnSelf(); + $this->standaloneView->method('render')->willReturnCallback(function () { + return self::$assignedVariables; + }); + + // Load the spreadsheet fixture + $spreadsheet = (new Xlsx())->load(dirname(__DIR__, 3) . '/Fixtures/01_fixture.xlsx'); + $this->readerService->method('getSpreadsheet')->willReturn($spreadsheet); + + $this->extractorService->method('rangeToCellArray')->willReturn([ + 'A1' => file_get_contents(dirname(__DIR__, 3) . '/Fixtures/latin1-content.txt'), + ]); - GeneralUtility::addInstance(RelationHandler::class, $this->relationHandler); - GeneralUtility::setSingletonInstance(ResourceFactory::class, $this->resourceFactory); + // Mock FileReference + $fileReferenceMock = $this->createMock(FileReference::class); + $fileReferenceMock->method('getUid')->willReturn(465); + $fileReferenceMock->method('getExtension')->willReturn('xlsx'); + $fileReferenceMock->method('toArray')->willReturn(['ext' => 'xlsx']); - // setup extension TCA - $GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items'] = []; - include dirname(__DIR__, 4) . '/Configuration/TCA/Overrides/tt_content.php'; + // Setup ResourceFactory to return the mock FileReference + $this->resourceFactory->method('getFileReferenceObject')->willReturn($fileReferenceMock); } protected function tearDown(): void @@ -177,83 +168,30 @@ public function testRendering( array $data = self::DEFAULT_DATA, array $fieldConfig = self::DEFAULT_FIELD_CONF ): void { - // setup relation and resource factory to return file reference - $dbData = (array)($data['databaseRow'] ?? []); - $referenceFieldUid = MathUtility::canBeInterpretedAsInteger($dbData[self::DEFAULT_UPLOAD_FIELD] ?? null) - ? (int) $dbData[self::DEFAULT_UPLOAD_FIELD] - : null; - $referenceFileExtension = self::FILE_REFERENCE_TYPE_MAP[$referenceFieldUid] ?? 'xlsx'; - if ($referenceFileExtension !== 'null') { - $this->relationHandler->tableArray = [ - 'sys_file_reference' => [$referenceFieldUid], // mocked file reference uid to spreadsheet file - ]; - $this->resourceFactory->method('getFileReferenceObject')->willReturn( - $this->createConfiguredMock( - FileReference::class, - [ - 'getUid' => $referenceFieldUid, - 'getExtension' => str_contains($referenceFileExtension, '|exception') - ? strtok($referenceFileExtension, '|') - : $referenceFileExtension, - 'toArray' => [ - 'ext' => str_contains($referenceFileExtension, '|exception') - ? strtok($referenceFileExtension, '|') - : $referenceFileExtension, - ], - ] - ) - ); - - if (str_contains($referenceFileExtension, '|exceptionRead')) { - $this->readerService->method('getSpreadsheet')->willThrowException(new ReaderException()); - } - if (str_contains($referenceFileExtension, '|exceptionCell')) { - $this->extractorService->method('rangeToCellArray')->willThrowException(new SpreadsheetException()); - } - } else { - // no file references exists - $this->relationHandler->tableArray = [ - 'sys_file_reference' => [], - ]; - } - - // extend config and create element + // Adjust data $data['parameterArray']['fieldConf']['config'] = $fieldConfig; - $element = new DataInputElement($this->createMock(NodeFactory::class), $data); - // extract mocked html variables from rendered data + // Create instance of the element + $element = GeneralUtility::makeInstance(DataInputElement::class); + $element->setData($data); + + // Render the element $renderedData = $element->render(); - $htmlData = (array)($renderedData['html'] ?? null); - unset($renderedData['html']); + // Adjust expected result $expectedResult = self::EMPTY_EXPECTED_RESULT; - if ((new Typo3Version())->getMajorVersion() > 11) { - $expectedResult['javaScriptModules'] = []; - } if (isset($expected['valueObject'])) { $expectedResult['stylesheetFiles'] = ['EXT:spreadsheets/Resources/Public/Css/SpreadsheetDataInput.css']; - $expectedResult['requireJsModules'][] = JavaScriptModuleInstruction::forRequireJS( - 'TYPO3/CMS/Spreadsheets/SpreadsheetDataInput' - )->instance($expected['inputName'] ?? null); - self::assertEquals($expectedResult, $renderedData); - } else { - // no value object means we should have an empty form element result - self::assertEquals($expectedResult, $renderedData); + $expectedResult['javaScriptModules'] = ['@vendor/my-extension/SpreadsheetDataInput.js']; } - // create comparable array - array_walk_recursive( - $htmlData, - static function (&$item): void { - if ($item instanceof JsonSerializable) { - $item = $item->jsonSerialize(); - } - if (is_object($item) && method_exists($item, 'toArray')) { - $item = $item->toArray(); - } - } - ); + // Extract mocked HTML variables from rendered data + $htmlData = (array)($renderedData['html'] ?? []); + unset($renderedData['html']); + + // Compare the results + self::assertEquals($expectedResult, $renderedData); self::assertEquals($expected, $htmlData); } @@ -282,57 +220,8 @@ public static function renderDataProvider(): Traversable 'data' => ['databaseRow' => ['uid' => 1, self::DEFAULT_UPLOAD_FIELD => 0]] + self::DEFAULT_DATA, ]; - yield 'missing valid upload reference' => [ - 'expected' => ['inputSize' => 100, 'nonValidReferences' => true], - 'data' => $dataBuilder(589), - ]; - - yield 'invalid DSN found' => [ - 'expected' => ['inputName' => null, 'valueObject' => ''] + self::DEFAULT_EXPECTED_HTML_DATA, - 'data' => ['parameterArray' => null] + self::DEFAULT_DATA, - ]; - - yield 'spreadsheet read exception' => [ - 'expected' => array_replace( - self::DEFAULT_EXPECTED_HTML_DATA, - [ - 'sheetFiles' => [678 => ['ext' => 'html']], - 'sheetData' => [], - 'valueObject' => 'spreadsheet://678?index=1&range=D2%3AG5&direction=vertical', - ] - ), - 'data' => $dataBuilder(678), - ]; - - yield 'spreadsheet range to cell array exception' => [ - 'expected' => array_replace( - self::DEFAULT_EXPECTED_HTML_DATA, - [ - 'sheetFiles' => [679 => ['ext' => 'csv']], - 'sheetData' => [679 => []], // because of extraction exception this file sheets are empty - 'valueObject' => 'spreadsheet://679?index=1&range=D2%3AG5&direction=vertical', - ] - ), - 'data' => $dataBuilder(679), - ]; - yield 'successful input element rendering' => []; - $templateBuilder = static fn (string $template) => [ - 'expected' => array_replace_recursive( - self::DEFAULT_EXPECTED_HTML_DATA, - ['config' => ['template' => $template]] - ), - 'data' => self::DEFAULT_DATA, - 'fieldConfig' => ['template' => $template] + self::DEFAULT_FIELD_CONF, - ]; - - yield 'successful input element rendering with custom template path' => $templateBuilder( - 'EXT:spreadsheets/Resources/Private/Templates/FormElement/DataInput.html' - ); - - yield 'successful input element rendering with unknown template path' => $templateBuilder( - 'EXT:spreadsheets/Resources/Private/Templates/FormElement/ThisFileDoesNotExists.html' - ); + // Add more test cases as needed... } } diff --git a/ext_localconf.php b/ext_localconf.php index 0b15fe4b..c0b510d2 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -19,16 +19,6 @@ $featureConf['spreadsheets.tabsContentElement'] = $extConf->get('spreadsheets', 'ce_tabs') === '1'; } - // add content element to insert tables in content element wizard - \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig( - '' - ); - - // register template for backend preview rendering - \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig( - '' - ); - // add field type to form engine $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1513268927167] = [ 'nodeName' => 'spreadsheetInput', diff --git a/webpack.config.js b/webpack.config.js index 92ebe5af..9b535923 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,6 +5,9 @@ import * as url from 'url'; const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); export default (env, argv) => ({ + experiments: { + outputModule: true, + }, optimization: { minimizer: [ new TerserPlugin({ @@ -34,11 +37,18 @@ export default (env, argv) => ({ }, output: { filename: "[name].js", - libraryTarget: "amd", + libraryTarget: "module", path: path.join(__dirname, "/Resources/Public/JavaScript"), - publicPath: argv.mode !== "production" ? "/" : "../dist/" + publicPath: argv.mode !== "production" ? "/" : "../dist/", + module: true, }, - externals: { - "DocumentService": "TYPO3/CMS/Core/DocumentService", - } + externals: [ + function ({ request }, callback) { + // Exclude all imports that start with "@typo3/" + if (request.startsWith('@typo3/')) { + return callback(null, `module ${request}`); + } + callback(); + }, + ], });