From e2584b9af255612bff444099d0699e5f0aeafdb7 Mon Sep 17 00:00:00 2001 From: Frank Naegler <frank.naegler@typo3.org> Date: Sun, 30 Jul 2017 01:58:17 +0200 Subject: [PATCH] [TASK] Refactor GridEditor.js with TypeScript Resolves: #82088 Releases: master Change-Id: Ie0ad7a8ec6ed3f67300e88b8b8e0711c4f3dbbd2 Reviewed-on: https://review.typo3.org/53622 Tested-by: TYPO3com <no-reply@typo3.com> Reviewed-by: Andreas Fernandez <typo3@scripting-base.de> Tested-by: Andreas Fernandez <typo3@scripting-base.de> Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl> Reviewed-by: Benni Mack <benni@typo3.org> Tested-by: Benni Mack <benni@typo3.org> --- Build/types/TYPO3/index.d.ts | 3 + .../Element/BackendLayoutWizardElement.php | 2 +- .../Private/TypeScript/GridEditor.ts | 932 ++++++++++ .../Resources/Public/JavaScript/GridEditor.js | 1636 +++++++++-------- .../Tests/JavaScript/GridEditorTest.js | 69 +- .../Tests/TypeScript/GridEditorTest.ts | 28 + 6 files changed, 1819 insertions(+), 851 deletions(-) create mode 100644 typo3/sysext/backend/Resources/Private/TypeScript/GridEditor.ts create mode 100644 typo3/sysext/backend/Tests/TypeScript/GridEditorTest.ts diff --git a/Build/types/TYPO3/index.d.ts b/Build/types/TYPO3/index.d.ts index 87d5e16fb1e4..d71ce151788f 100644 --- a/Build/types/TYPO3/index.d.ts +++ b/Build/types/TYPO3/index.d.ts @@ -19,8 +19,10 @@ declare namespace TYPO3 { export class Modal { public readonly sizes: {[key: string]: string}; public readonly styles: {[key: string]: string}; + public currentModal: JQuery; public advanced(configuration: object): any; public confirm(title: string, content: any, severity: number, buttons: any[], additionalCssClasses?: string[]): JQuery; // tslint:disable-line:max-line-length + public show(title: string, content: any, severity: number, buttons: any[], additionalCssClasses?: string[]): JQuery; // tslint:disable-line:max-line-length public dismiss(): void; } export class Severity { @@ -29,6 +31,7 @@ declare namespace TYPO3 { public readonly ok: number; public readonly warning: number; public readonly: number; + public getCssClass(severity: number): string; } } } diff --git a/typo3/sysext/backend/Classes/View/Wizard/Element/BackendLayoutWizardElement.php b/typo3/sysext/backend/Classes/View/Wizard/Element/BackendLayoutWizardElement.php index f1e0cfea082a..4cf561b2322e 100644 --- a/typo3/sysext/backend/Classes/View/Wizard/Element/BackendLayoutWizardElement.php +++ b/typo3/sysext/backend/Classes/View/Wizard/Element/BackendLayoutWizardElement.php @@ -138,7 +138,7 @@ class BackendLayoutWizardElement extends AbstractFormElement $html = implode(LF, $html); $resultArray['html'] = $html; - $resultArray['requireJsModules'][] = 'TYPO3/CMS/Backend/GridEditor'; + $resultArray['requireJsModules'][] = ['TYPO3/CMS/Backend/GridEditor' => 'function(GridEditor) { new GridEditor.GridEditor(); }']; $resultArray['additionalInlineLanguageLabelFiles'][] = 'EXT:lang/Resources/Private/Language/locallang_wizards.xlf'; $resultArray['additionalInlineLanguageLabelFiles'][] = 'EXT:backend/Resources/Private/Language/locallang.xlf'; diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/GridEditor.ts b/typo3/sysext/backend/Resources/Private/TypeScript/GridEditor.ts new file mode 100644 index 000000000000..78bdb0ec10d7 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/TypeScript/GridEditor.ts @@ -0,0 +1,932 @@ +/* + * 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! + */ + +import 'bootstrap'; +import Modal = require('TYPO3/CMS/Backend/Modal'); +import Severity = require('TYPO3/CMS/Backend/Severity'); +import $ = require('jquery'); + +/** + * GridEditorConfigurationInterface + */ +interface GridEditorConfigurationInterface { + nameLabel: string; + columnLabel: string; +} + +/** + * CellInterface + */ +interface CellInterface { + spanned: number; + rowspan: number; + colspan: number; + column: number; + name: string; + colpos: string; +} + +/** + * Module: TYPO3/CMS/Backend/GridEditor + * @exports TYPO3/CMS/Backend/GridEditor + */ +export class GridEditor { + + /** + * Remove all markup + * + * @param {String} input + * @returns {string} + */ + public static stripMarkup(input: string): string { + input = input.replace(/<(.*)>/gi, ''); + return $('<p>' + input + '</p>').text(); + } + + protected colCount = 1; + protected rowCount = 1; + protected field: JQuery; + protected data: any[]; + protected nameLabel = 'name'; + protected columnLabel = 'columen label'; + protected targetElement: JQuery; + protected defaultCell: object = {spanned: 0, rowspan: 1, colspan: 1, name: '', colpos: '', column: undefined}; + protected selectorEditor = '.t3js-grideditor'; + protected selectorAddColumn = '.t3js-grideditor-addcolumn'; + protected selectorRemoveColumn = '.t3js-grideditor-removecolumn'; + protected selectorAddRowTop = '.t3js-grideditor-addrow-top'; + protected selectorRemoveRowTop = '.t3js-grideditor-removerow-top'; + protected selectorAddRowBottom = '.t3js-grideditor-addrow-bottom'; + protected selectorRemoveRowBottom = '.t3js-grideditor-removerow-bottom'; + protected selectorLinkEditor = '.t3js-grideditor-link-editor'; + protected selectorLinkExpandRight = '.t3js-grideditor-link-expand-right'; + protected selectorLinkShrinkLeft = '.t3js-grideditor-link-shrink-left'; + protected selectorLinkExpandDown = '.t3js-grideditor-link-expand-down'; + protected selectorLinkShrinkUp = '.t3js-grideditor-link-shrink-up'; + protected selectorDocHeaderSave = '.t3js-grideditor-savedok'; + protected selectorDocHeaderSaveClose = '.t3js-grideditor-savedokclose'; + protected selectorConfigPreview = '.t3js-grideditor-preview-config'; + protected selectorConfigPreviewButton = '.t3js-grideditor-preview-button'; + + /** + * + * @param {GridEditorConfigurationInterface} config + */ + constructor(config: GridEditorConfigurationInterface = null) { + const $element = $(this.selectorEditor); + this.colCount = $element.data('colcount'); + this.rowCount = $element.data('rowcount'); + this.field = $('input[name="' + $element.data('field') + '"]'); + this.data = $element.data('data'); + this.nameLabel = config !== null ? config.nameLabel : 'Name'; + this.columnLabel = config !== null ? config.columnLabel : 'Column'; + this.targetElement = $(this.selectorEditor); + $(this.selectorConfigPreview).hide(); + + $(this.selectorConfigPreviewButton).empty().append(TYPO3.lang['button.showPageTsConfig']); + + this.initializeEvents(); + this.drawTable(); + this.writeConfig(this.export2LayoutRecord()); + } + + /** + * + */ + protected initializeEvents(): void { + $(document).on('click', this.selectorAddColumn, this.addColumnHandler); + $(document).on('click', this.selectorRemoveColumn, this.removeColumnHandler); + $(document).on('click', this.selectorAddRowTop, this.addRowTopHandler); + $(document).on('click', this.selectorAddRowBottom, this.addRowBottomHandler); + $(document).on('click', this.selectorRemoveRowTop, this.removeRowTopHandler); + $(document).on('click', this.selectorRemoveRowBottom, this.removeRowBottomHandler); + $(document).on('click', this.selectorLinkEditor, this.linkEditorHandler); + $(document).on('click', this.selectorLinkExpandRight, this.linkExpandRightHandler); + $(document).on('click', this.selectorLinkShrinkLeft, this.linkShrinkLeftHandler); + $(document).on('click', this.selectorLinkExpandDown, this.linkExpandDownHandler); + $(document).on('click', this.selectorLinkShrinkUp, this.linkShrinkUpHandler); + $(document).on('click', this.selectorConfigPreviewButton, this.configPreviewButtonHandler); + } + + /** + * + * @param {Event} e + */ + protected modalButtonClickHandler = (e: Event) => { + const button: any = e.target; + if (button.name === 'cancel') { + Modal.currentModal.trigger('modal-dismiss'); + } else if (button.name === 'ok') { + this.setName( + Modal.currentModal.find('.t3js-grideditor-field-name').val(), + Modal.currentModal.data('col'), + Modal.currentModal.data('row'), + ); + this.setColumn( + Modal.currentModal.find('.t3js-grideditor-field-colpos').val(), + Modal.currentModal.data('col'), + Modal.currentModal.data('row'), + ); + this.drawTable(); + this.writeConfig(this.export2LayoutRecord()); + Modal.currentModal.trigger('modal-dismiss'); + } + } + + /** + * + * @param {Event} e + */ + protected addColumnHandler = (e: Event) => { + e.preventDefault(); + this.addColumn(); + this.drawTable(); + this.writeConfig(this.export2LayoutRecord()); + } + + /** + * + * @param {Event} e + */ + protected removeColumnHandler = (e: Event) => { + e.preventDefault(); + this.removeColumn(); + this.drawTable(); + this.writeConfig(this.export2LayoutRecord()); + } + + /** + * + * @param {Event} e + */ + protected addRowTopHandler = (e: Event) => { + e.preventDefault(); + this.addRowTop(); + this.drawTable(); + this.writeConfig(this.export2LayoutRecord()); + } + + /** + * + * @param {Event} e + */ + protected addRowBottomHandler = (e: Event) => { + e.preventDefault(); + this.addRowBottom(); + this.drawTable(); + this.writeConfig(this.export2LayoutRecord()); + } + + /** + * + * @param {Event} e + */ + protected removeRowTopHandler = (e: Event) => { + e.preventDefault(); + this.removeRowTop(); + this.drawTable(); + this.writeConfig(this.export2LayoutRecord()); + } + + /** + * + * @param {Event} e + */ + protected removeRowBottomHandler = (e: Event) => { + e.preventDefault(); + this.removeRowBottom(); + this.drawTable(); + this.writeConfig(this.export2LayoutRecord()); + } + + /** + * + * @param {Event} e + */ + protected linkEditorHandler = (e: Event) => { + e.preventDefault(); + const $element = $(e.target); + this.showOptions($element.data('col'), $element.data('row')); + } + + /** + * + * @param {Event} e + */ + protected linkExpandRightHandler = (e: Event) => { + e.preventDefault(); + const $element = $(e.target); + this.addColspan($element.data('col'), $element.data('row')); + this.drawTable(); + this.writeConfig(this.export2LayoutRecord()); + } + + /** + * + * @param {Event} e + */ + protected linkShrinkLeftHandler = (e: Event) => { + e.preventDefault(); + const $element = $(e.target); + this.removeColspan($element.data('col'), $element.data('row')); + this.drawTable(); + this.writeConfig(this.export2LayoutRecord()); + } + + /** + * + * @param {Event} e + */ + protected linkExpandDownHandler = (e: Event) => { + e.preventDefault(); + const $element = $(e.target); + this.addRowspan($element.data('col'), $element.data('row')); + this.drawTable(); + this.writeConfig(this.export2LayoutRecord()); + } + + /** + * + * @param {Event} e + */ + protected linkShrinkUpHandler = (e: Event) => { + e.preventDefault(); + const $element = $(e.target); + this.removeRowspan($element.data('col'), $element.data('row')); + this.drawTable(); + this.writeConfig(this.export2LayoutRecord()); + } + + /** + * + * @param {Event} e + */ + protected configPreviewButtonHandler = (e: Event) => { + e.preventDefault(); + const $preview = $(this.selectorConfigPreview); + const $button = $(this.selectorConfigPreviewButton); + if ($preview.is(':visible')) { + $button.empty().append(TYPO3.lang['button.showPageTsConfig']); + $(this.selectorConfigPreview).slideUp(); + } else { + $button.empty().append(TYPO3.lang['button.hidePageTsConfig']); + $(this.selectorConfigPreview).slideDown(); + } + } + + /** + * Create a new cell from defaultCell + * @returns {Object} + */ + protected getNewCell() { + return $.extend({}, this.defaultCell); + } + + /** + * write data back to hidden field + * + * @param data + */ + protected writeConfig(data: any) { + this.field.val(data); + const configLines = data.split('\n'); + let config = ''; + for (const line of configLines) { + if (line) { + config += '\t\t\t' + line + '\n'; + } + } + $(this.selectorConfigPreview).find('code').empty().append( + 'mod.web_layout.BackendLayouts {\n' + + ' exampleKey {\n' + + ' title = Example\n' + + ' icon = EXT:example_extension/Resources/Public/Images/BackendLayouts/default.gif\n' + + ' config {\n' + + config.replace(new RegExp('\t', 'g'), ' ') + + ' }\n' + + ' }\n' + + '}\n', + ); + } + + /** + * Add a new row at the top + */ + protected addRowTop() { + const newRow = []; + for (let i = 0; i < this.colCount; i++) { + const newCell = this.getNewCell(); + newCell.name = i + 'x' + this.data.length; + newRow[i] = newCell; + } + this.data.unshift(newRow); + this.rowCount++; + } + + /** + * Add a new row at the bottom + */ + protected addRowBottom() { + const newRow = []; + for (let i = 0; i < this.colCount; i++) { + const newCell = this.getNewCell(); + newCell.name = i + 'x' + this.data.length; + newRow[i] = newCell; + } + this.data.push(newRow); + this.rowCount++; + } + + /** + * Removes the first row of the grid and adjusts all cells that might be effected + * by that change. (Removing colspans) + */ + protected removeRowTop(): boolean { + if (this.rowCount <= 1) { + return false; + } + const newData = []; + for (let rowIndex = 1; rowIndex < this.rowCount; rowIndex++) { + newData.push(this.data[rowIndex]); + } + + // fix rowspan in former last row + for (let colIndex = 0; colIndex < this.colCount; colIndex++) { + if (this.data[0][colIndex].spanned === 1) { + this.findUpperCellWidthRowspanAndDecreaseByOne(colIndex, 0); + } + } + + this.data = newData; + this.rowCount--; + return true; + } + + /** + * Removes the last row of the grid and adjusts all cells that might be effected + * by that change. (Removing colspans) + */ + protected removeRowBottom(): boolean { + if (this.rowCount <= 1) { + return false; + } + const newData = []; + for (let rowIndex = 0; rowIndex < this.rowCount - 1; rowIndex++) { + newData.push(this.data[rowIndex]); + } + + // fix rowspan in former last row + for (let colIndex = 0; colIndex < this.colCount; colIndex++) { + if (this.data[this.rowCount - 1][colIndex].spanned === 1) { + this.findUpperCellWidthRowspanAndDecreaseByOne(colIndex, this.rowCount - 1); + } + } + + this.data = newData; + this.rowCount--; + return true; + } + + /** + * Takes a cell and looks above it if there are any cells that have colspans that + * spans into the given cell. This is used when a row was removed from the grid + * to make sure that no cell with wrong colspans exists in the grid. + * + * @param {number} col + * @param {number} row integer + */ + protected findUpperCellWidthRowspanAndDecreaseByOne(col: number, row: number): boolean { + const upperCell = this.getCell(col, row - 1); + if (!upperCell) { + return false; + } + + if (upperCell.spanned === 1) { + this.findUpperCellWidthRowspanAndDecreaseByOne(col, row - 1); + } else { + if (upperCell.rowspan > 1) { + this.removeRowspan(col, row - 1); + } + } + return true; + } + + /** + * Removes the outermost right column from the grid. + */ + protected removeColumn(): boolean { + if (this.colCount <= 1) { + return false; + } + const newData = []; + + for (let rowIndex = 0; rowIndex < this.rowCount; rowIndex++) { + const newRow = []; + for (let colIndex = 0; colIndex < this.colCount - 1; colIndex++) { + newRow.push(this.data[rowIndex][colIndex]); + } + if (this.data[rowIndex][this.colCount - 1].spanned === 1) { + this.findLeftCellWidthColspanAndDecreaseByOne(this.colCount - 1, rowIndex); + } + newData.push(newRow); + } + + this.data = newData; + this.colCount--; + return true; + } + + /** + * Checks if there are any cells on the left side of a given cell with a + * rowspan that spans over the given cell. + * + * @param {number} col + * @param {number} row + */ + protected findLeftCellWidthColspanAndDecreaseByOne(col: number, row: number): boolean { + const leftCell = this.getCell(col - 1, row); + if (!leftCell) { + return false; + } + + if (leftCell.spanned === 1) { + this.findLeftCellWidthColspanAndDecreaseByOne(col - 1, row); + } else { + if (leftCell.colspan > 1) { + this.removeColspan(col - 1, row); + } + } + return true; + } + + /** + * Adds a column at the right side of the grid. + */ + protected addColumn(): void { + for (let rowIndex = 0; rowIndex < this.rowCount; rowIndex++) { + const newCell = this.getNewCell(); + newCell.name = this.colCount + 'x' + rowIndex; + this.data[rowIndex].push(newCell); + } + this.colCount++; + } + + /** + * Draws the grid as table into a given container. + * It also adds all needed links and bindings to the cells to make it editable. + */ + protected drawTable(): void { + const $colgroup = $('<colgroup>'); + for (let col = 0; col < this.colCount; col++) { + const percent = 100 / this.colCount; + $colgroup.append($('<col>').css({ + width: parseInt(percent.toString(), 10) + '%', + })); + } + const $table = $('<table id="base" class="table editor">'); + $table.append($colgroup); + + for (let row = 0; row < this.rowCount; row++) { + const rowData = this.data[row]; + if (rowData.length === 0) { + continue; + } + + const $row = $('<tr>'); + + for (let col = 0; col < this.colCount; col++) { + const cell = this.data[row][col]; + if (cell.spanned === 1) { + continue; + } + const percentRow = 100 / this.rowCount; + const percentCol = 100 / this.colCount; + const $cell = $('<td>').css({ + height: parseInt(percentRow.toString(), 10) * cell.rowspan + '%', + width: parseInt(percentCol.toString(), 10) * cell.colspan + '%', + }); + const $container = $('<div class="cell_container">'); + $cell.append($container); + const $anchor = $('<a href="#" data-col="' + col + '" data-row="' + row + '">'); + + $container.append( + $anchor + .clone() + .attr('class', 't3js-grideditor-link-editor link link_editor') + .attr('title', TYPO3.lang.grid_editCell), + ); + if (this.cellCanSpanRight(col, row)) { + $container.append( + $anchor + .clone() + .attr('class', 't3js-grideditor-link-expand-right link link_expand_right') + .attr('title', TYPO3.lang.grid_mergeCell), + ); + } + if (this.cellCanShrinkLeft(col, row)) { + $container.append( + $anchor + .clone() + .attr('class', 't3js-grideditor-link-shrink-left link link_shrink_left') + .attr('title', TYPO3.lang.grid_splitCell), + ); + } + if (this.cellCanSpanDown(col, row)) { + $container.append( + $anchor + .clone() + .attr('class', 't3js-grideditor-link-expand-down link link_expand_down') + .attr('title', TYPO3.lang.grid_mergeCell), + ); + } + if (this.cellCanShrinkUp(col, row)) { + $container.append( + $anchor + .clone() + .attr('class', 't3js-grideditor-link-shrink-up link link_shrink_up') + .attr('title', TYPO3.lang.grid_splitCell), + ); + } + $cell.append( + $('<div class="cell_data">') + .html( + TYPO3.lang.grid_name + ': ' + + (cell.name ? GridEditor.stripMarkup(cell.name) : TYPO3.lang.grid_notSet) + + '<br />' + + TYPO3.lang.grid_column + ': ' + + (typeof cell.column === 'undefined' || isNaN(cell.column) + ? TYPO3.lang.grid_notSet + : parseInt(cell.column, 10) + ), + ), + ); + if (cell.colspan > 1) { + $cell.attr('colspan', cell.colspan); + } + if (cell.rowspan > 1) { + $cell.attr('rowspan', cell.rowspan); + } + $row.append($cell); + } + $table.append($row); + } + $(this.targetElement).empty().append($table); + } + + /** + * Sets the name of a certain grid element. + * + * @param {String} newName + * @param {number} col + * @param {number} row + * + * @returns {Boolean} + */ + protected setName(newName: string, col: number, row: number): boolean { + const cell = this.getCell(col, row); + if (!cell) { + return false; + } + cell.name = GridEditor.stripMarkup(newName); + return true; + } + + /** + * Sets the column field for a certain grid element. This is NOT the column of the + * element itself. + * + * @param {number} newColumn + * @param {number} col + * @param {number} row + * + * @returns {Boolean} + */ + protected setColumn(newColumn: number, col: number, row: number): boolean { + const cell = this.getCell(col, row); + if (!cell) { + return false; + } + cell.column = parseInt(newColumn.toString(), 10); + return true; + } + + /** + * Creates an ExtJs Window with two input fields and shows it. On save, the data + * is written into the grid element. + * + * @param {number} col + * @param {number} row + * + * @returns {Boolean} + */ + protected showOptions(col: number, row: number): boolean { + const cell = this.getCell(col, row); + if (!cell) { + return false; + } + let colPos; + if (cell.column === 0) { + colPos = 0; + } else if (cell.column) { + colPos = parseInt(cell.column.toString(), 10); + } else { + colPos = ''; + } + + const $markup = $('<div>'); + const $formGroup = $('<div class="form-group">'); + const $label = $('<label>'); + const $input = $('<input>'); + + $markup.append([ + $formGroup + .clone() + .append([ + $label + .clone() + .text(TYPO3.lang.grid_nameHelp) + , + $input + .clone() + .attr('type', 'text') + .attr('class', 't3js-grideditor-field-name form-control') + .attr('name', 'name') + .val(GridEditor.stripMarkup(cell.name) || ''), + ]), + $formGroup + .clone() + .append([ + $label + .clone() + .text(TYPO3.lang.grid_columnHelp) + , + $input + .clone() + .attr('type', 'text') + .attr('class', 't3js-grideditor-field-colpos form-control') + .attr('name', 'column') + .val(colPos), + ]), + ]); + + const $modal = Modal.show(TYPO3.lang.grid_windowTitle, $markup, Severity.notice, [ + { + active: true, + btnClass: 'btn-default', + name: 'cancel', + text: $(this).data('button-close-text') || TYPO3.lang['button.cancel'] || 'Cancel', + }, + { + btnClass: 'btn-' + Severity.getCssClass(Severity.notice), + name: 'ok', + text: $(this).data('button-ok-text') || TYPO3.lang['button.ok'] || 'OK', + }, + ]); + $modal.data('col', col); + $modal.data('row', row); + $modal.on('button.clicked', this.modalButtonClickHandler); + return true; + } + + /** + * Returns a cell element from the grid. + * + * @param {number} col + * @param {number} row + */ + protected getCell(col: number, row: number): any { + if (col > this.colCount - 1) { + return false; + } + if (row > this.rowCount - 1) { + return false; + } + if (this.data.length > row - 1 && this.data[row].length > col - 1) { + return this.data[row][col]; + } + return null; + } + + /** + * Checks whether a cell can span to the right or not. A cell can span to the right + * if it is not in the last column and if there is no cell beside it that is + * already overspanned by some other cell. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + protected cellCanSpanRight(col: number, row: number): boolean { + if (col === this.colCount - 1) { + return false; + } + + const cell = this.getCell(col, row); + let checkCell; + if (cell.rowspan > 1) { + for (let rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) { + checkCell = this.getCell(col + cell.colspan, rowIndex); + if (!checkCell || checkCell.spanned === 1 || checkCell.colspan > 1 || checkCell.rowspan > 1) { + return false; + } + } + } else { + checkCell = this.getCell(col + cell.colspan, row); + if (!checkCell || cell.spanned === 1 || checkCell.spanned === 1 || checkCell.colspan > 1 + || checkCell.rowspan > 1) { + return false; + } + } + + return true; + } + + /** + * Checks whether a cell can span down or not. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + protected cellCanSpanDown(col: number, row: number): boolean { + if (row === this.rowCount - 1) { + return false; + } + + const cell = this.getCell(col, row); + let checkCell; + if (cell.colspan > 1) { + // we have to check all cells on the right side for the complete colspan + for (let colIndex = col; colIndex < col + cell.colspan; colIndex++) { + checkCell = this.getCell(colIndex, row + cell.rowspan); + if (!checkCell || checkCell.spanned === 1 || checkCell.colspan > 1 || checkCell.rowspan > 1) { + return false; + } + } + } else { + checkCell = this.getCell(col, row + cell.rowspan); + if (!checkCell || cell.spanned === 1 || checkCell.spanned === 1 || checkCell.colspan > 1 + || checkCell.rowspan > 1) { + return false; + } + } + + return true; + } + + /** + * Checks if a cell can shrink to the left. It can shrink if the colspan of the + * cell is bigger than 1. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + protected cellCanShrinkLeft(col: number, row: number): boolean { + return (this.data[row][col].colspan > 1); + } + + /** + * Returns if a cell can shrink up. This is the case if a cell has at least + * a rowspan of 2. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + protected cellCanShrinkUp(col: number, row: number): boolean { + return (this.data[row][col].rowspan > 1); + } + + /** + * Adds a colspan to a grid element. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + protected addColspan(col: number, row: number): boolean { + const cell = this.getCell(col, row); + if (!cell || !this.cellCanSpanRight(col, row)) { + return false; + } + + for (let rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) { + this.data[rowIndex][col + cell.colspan].spanned = 1; + } + cell.colspan += 1; + return true; + } + + /** + * Adds a rowspan to grid element. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + protected addRowspan(col: number, row: number): boolean { + const cell = this.getCell(col, row); + if (!cell || !this.cellCanSpanDown(col, row)) { + return false; + } + + for (let colIndex = col; colIndex < col + cell.colspan; colIndex++) { + this.data[row + cell.rowspan][colIndex].spanned = 1; + } + cell.rowspan += 1; + return true; + } + + /** + * Removes a colspan from a grid element. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + protected removeColspan(col: number, row: number): boolean { + const cell = this.getCell(col, row); + if (!cell || !this.cellCanShrinkLeft(col, row)) { + return false; + } + + cell.colspan -= 1; + + for (let rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) { + this.data[rowIndex][col + cell.colspan].spanned = 0; + } + return true; + } + + /** + * Removes a rowspan from a grid element. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + protected removeRowspan(col: number, row: number): boolean { + const cell = this.getCell(col, row); + if (!cell || !this.cellCanShrinkUp(col, row)) { + return false; + } + + cell.rowspan -= 1; + for (let colIndex = col; colIndex < col + cell.colspan; colIndex++) { + this.data[row + cell.rowspan][colIndex].spanned = 0; + } + return true; + } + + /** + * Exports the current grid to a TypoScript notation that can be read by the + * page module and is human readable. + * + * @returns {String} + */ + protected export2LayoutRecord(): string { + let result = 'backend_layout {\n\tcolCount = ' + this.colCount + '\n\trowCount = ' + this.rowCount + '\n\trows {\n'; + for (let row = 0; row < this.rowCount; row++) { + result += '\t\t' + (row + 1) + ' {\n'; + result += '\t\t\tcolumns {\n'; + let colIndex = 0; + for (let col = 0; col < this.colCount; col++) { + const cell = this.getCell(col, row); + if (cell) { + if (!cell.spanned) { + colIndex++; + result += '\t\t\t\t' + (colIndex) + ' {\n'; + result += '\t\t\t\t\tname = ' + ((!cell.name) ? col + 'x' + row : cell.name) + '\n'; + if (cell.colspan > 1) { + result += '\t\t\t\t\tcolspan = ' + cell.colspan + '\n'; + } + if (cell.rowspan > 1) { + result += '\t\t\t\t\trowspan = ' + cell.rowspan + '\n'; + } + if (typeof(cell.column) === 'number') { + result += '\t\t\t\t\tcolPos = ' + cell.column + '\n'; + } + result += '\t\t\t\t}\n'; + } + } + + } + result += '\t\t\t}\n'; + result += '\t\t}\n'; + } + + result += '\t}\n}\n'; + return result; + } +} diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/GridEditor.js b/typo3/sysext/backend/Resources/Public/JavaScript/GridEditor.js index 31e2c44d1116..dc0d17516d7d 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/GridEditor.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/GridEditor.js @@ -10,811 +10,835 @@ * * The TYPO3 project - inspiring people to share! */ - -/** - * Module: TYPO3/CMS/Backend/GridEditor - */ -define(['jquery', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/Severity', 'bootstrap'], function($, Modal, Severity) { - 'use strict'; - - /** - * The main ContextHelp object - * - * @type {{selectorEditor: string, selectorAddColumn: string, selectorRemoveColumn: string, selectorAddRowTop: string, selectorRemoveRowTop: string, selectorAddRowBottom: string, selectorRemoveRowBottom: string, selectorLinkEditor: string, selectorLinkExpandRight: string, selectorLinkShrinkLeft: string, selectorLinkExpandDown: string, selectorLinkShrinkUp: string, selectorDocHeaderSave: string, selectorDocHeaderSaveClose: string, selectorConfigPreview: string, selectorConfigPreviewButton: string, colCount: number, rowCount: number, field: string, data: Array, nameLabel: string, columnLabel: string, targetElement: null}} - * @exports TYPO3/CMS/Backend/GridEditor - */ - var GridEditor = { - selectorEditor: '.t3js-grideditor', - selectorAddColumn: '.t3js-grideditor-addcolumn', - selectorRemoveColumn: '.t3js-grideditor-removecolumn', - selectorAddRowTop: '.t3js-grideditor-addrow-top', - selectorRemoveRowTop: '.t3js-grideditor-removerow-top', - selectorAddRowBottom: '.t3js-grideditor-addrow-bottom', - selectorRemoveRowBottom: '.t3js-grideditor-removerow-bottom', - selectorLinkEditor: '.t3js-grideditor-link-editor', - selectorLinkExpandRight: '.t3js-grideditor-link-expand-right', - selectorLinkShrinkLeft: '.t3js-grideditor-link-shrink-left', - selectorLinkExpandDown: '.t3js-grideditor-link-expand-down', - selectorLinkShrinkUp: '.t3js-grideditor-link-shrink-up', - selectorDocHeaderSave: '.t3js-grideditor-savedok', - selectorDocHeaderSaveClose: '.t3js-grideditor-savedokclose', - selectorConfigPreview: '.t3js-grideditor-preview-config', - selectorConfigPreviewButton: '.t3js-grideditor-preview-button', - colCount: 1, - rowCount: 1, - field: '', - data: [], - nameLabel: 'name', - columnLabel: 'columen label', - targetElement: null, - defaultCell: {spanned: 0, rowspan: 1, colspan: 1, name: '', colpos: ''} - }; - - /** - * - * @param {Object} config - */ - GridEditor.initialize = function(config) { - config = config || {}; - var $element = $(GridEditor.selectorEditor); - GridEditor.colCount = $element.data('colcount'); - GridEditor.rowCount = $element.data('rowcount'); - GridEditor.field = $('input[name="' + $element.data('field') + '"]'); - GridEditor.data = $element.data('data'); - GridEditor.nameLabel = config.nameLabel || 'Name'; - GridEditor.columnLabel = config.columnLabel || 'Column'; - GridEditor.targetElement = $(GridEditor.selectorEditor); - $(GridEditor.selectorConfigPreview).hide(); - - $(document).on('click', GridEditor.selectorAddColumn, function(e) { - e.preventDefault(); - GridEditor.addColumn(); - GridEditor.drawTable(); - GridEditor.writeConfig(GridEditor.export2LayoutRecord()); - }); - $(document).on('click', GridEditor.selectorRemoveColumn, function(e) { - e.preventDefault(); - GridEditor.removeColumn(); - GridEditor.drawTable(); - GridEditor.writeConfig(GridEditor.export2LayoutRecord()); - }); - $(document).on('click', GridEditor.selectorAddRowTop, function (e) { - e.preventDefault(); - GridEditor.addRowTop(); - GridEditor.drawTable(); - GridEditor.writeConfig(GridEditor.export2LayoutRecord()); - }); - $(document).on('click', GridEditor.selectorAddRowBottom, function (e) { - e.preventDefault(); - GridEditor.addRowBottom(); - GridEditor.drawTable(); - GridEditor.writeConfig(GridEditor.export2LayoutRecord()); - }); - $(document).on('click', GridEditor.selectorRemoveRowTop, function (e) { - e.preventDefault(); - GridEditor.removeRowTop(); - GridEditor.drawTable(); - GridEditor.writeConfig(GridEditor.export2LayoutRecord()); - }); - $(document).on('click', GridEditor.selectorRemoveRowBottom, function (e) { - e.preventDefault(); - GridEditor.removeRowBottom(); - GridEditor.drawTable(); - GridEditor.writeConfig(GridEditor.export2LayoutRecord()); - }); - $(document).on('click', GridEditor.selectorLinkEditor, function(e) { - e.preventDefault(); - var $element = $(this); - var col = $element.data('col'); - var row = $element.data('row'); - GridEditor.showOptions(col, row); - }); - $(document).on('click', GridEditor.selectorLinkExpandRight, function(e) { - e.preventDefault(); - var $element = $(this); - var col = $element.data('col'); - var row = $element.data('row'); - GridEditor.addColspan(col, row); - GridEditor.drawTable(); - GridEditor.writeConfig(GridEditor.export2LayoutRecord()); - }); - $(document).on('click', GridEditor.selectorLinkShrinkLeft, function(e) { - e.preventDefault(); - var $element = $(this); - var col = $element.data('col'); - var row = $element.data('row'); - GridEditor.removeColspan(col, row); - GridEditor.drawTable(); - GridEditor.writeConfig(GridEditor.export2LayoutRecord()); - }); - $(document).on('click', GridEditor.selectorLinkExpandDown, function(e) { - e.preventDefault(); - var $element = $(this); - var col = $element.data('col'); - var row = $element.data('row'); - GridEditor.addRowspan(col, row); - GridEditor.drawTable(); - GridEditor.writeConfig(GridEditor.export2LayoutRecord()); - }); - $(document).on('click', GridEditor.selectorLinkShrinkUp, function(e) { - e.preventDefault(); - var $element = $(this); - var col = $element.data('col'); - var row = $element.data('row'); - GridEditor.removeRowspan(col, row); - GridEditor.drawTable(); - GridEditor.writeConfig(GridEditor.export2LayoutRecord()); - }); - - $(GridEditor.selectorConfigPreviewButton).empty().append(TYPO3.lang['button.showPageTsConfig']); - $(document).on('click', GridEditor.selectorConfigPreviewButton, function(e) { - e.preventDefault(); - var $preview = $(GridEditor.selectorConfigPreview); - var $button = $(GridEditor.selectorConfigPreviewButton); - if ($preview.is(':visible')) { - $button.empty().append(TYPO3.lang['button.showPageTsConfig']); - $(GridEditor.selectorConfigPreview).slideUp(); - } else { - $button.empty().append(TYPO3.lang['button.hidePageTsConfig']); - $(GridEditor.selectorConfigPreview).slideDown(); - } - - }); - - GridEditor.drawTable(); - GridEditor.writeConfig(GridEditor.export2LayoutRecord()); - }; - - /** - * Create a new cell from defaultCell - * @returns {Object} - */ - GridEditor.getNewCell = function() { - return $.extend({}, GridEditor.defaultCell); - }; - - /** - * write data back to hidden field - * - * @param data - */ - GridEditor.writeConfig = function(data) { - GridEditor.field.val(data); - var configLines = data.split('\n'); - var config = ''; - for (var i=0; i<configLines.length; i++) { - if (configLines[i].length) { - config += '\t\t\t' + configLines[i] + '\n'; - } - } - $(GridEditor.selectorConfigPreview).find('code').empty().append( - 'mod.web_layout.BackendLayouts {\n' + - ' exampleKey {\n' + - ' title = Example\n' + - ' icon = EXT:example_extension/Resources/Public/Images/BackendLayouts/default.gif\n' + - ' config {\n' + - config.replace(new RegExp('\t', 'g'), ' ') + - ' }\n' + - ' }\n' + - '}\n' - ); - }; - - /** - * Add a new row at the top - */ - GridEditor.addRowTop = function () { - var newRow = []; - for (var i = 0; i < GridEditor.colCount; i++) { - var newCell = GridEditor.getNewCell(); - newCell.name = i + 'x' + GridEditor.data.length; - newRow[i] = newCell; - } - GridEditor.data.unshift(newRow); - GridEditor.rowCount++; - }; - - /** - * Add a new row at the bottom - */ - GridEditor.addRowBottom = function() { - var newRow = []; - for (var i = 0; i < GridEditor.colCount; i++) { - var newCell = GridEditor.getNewCell(); - newCell.name = i + 'x' + GridEditor.data.length; - newRow[i] = newCell; - } - GridEditor.data.push(newRow); - GridEditor.rowCount++; - }; - - /** - * Removes the first row of the grid and adjusts all cells that might be effected - * by that change. (Removing colspans) - */ - GridEditor.removeRowTop = function () { - if (GridEditor.rowCount <= 1) { - return false; - } - var newData = []; - for (var rowIndex = 1; rowIndex < GridEditor.rowCount; rowIndex++) { - newData.push(GridEditor.data[rowIndex]); - } - - // fix rowspan in former last row - for (var colIndex = 0; colIndex < GridEditor.colCount; colIndex++) { - if (GridEditor.data[0][colIndex].spanned === 1) { - GridEditor.findUpperCellWidthRowspanAndDecreaseByOne(colIndex, 0); - } - } - - GridEditor.data = newData; - GridEditor.rowCount--; - }; - - /** - * Removes the last row of the grid and adjusts all cells that might be effected - * by that change. (Removing colspans) - */ - GridEditor.removeRowBottom = function () { - if (GridEditor.rowCount <= 1) { - return false; - } - var newData = []; - for (var rowIndex = 0; rowIndex < GridEditor.rowCount - 1; rowIndex++) { - newData.push(GridEditor.data[rowIndex]); - } - - // fix rowspan in former last row - for (var colIndex = 0; colIndex < GridEditor.colCount; colIndex++) { - if (GridEditor.data[GridEditor.rowCount - 1][colIndex].spanned === 1) { - GridEditor.findUpperCellWidthRowspanAndDecreaseByOne(colIndex, GridEditor.rowCount - 1); - } - } - - GridEditor.data = newData; - GridEditor.rowCount--; - }; - - /** - * Takes a cell and looks above it if there are any cells that have colspans that - * spans into the given cell. This is used when a row was removed from the grid - * to make sure that no cell with wrong colspans exists in the grid. - * - * @param {Integer} col - * @param {Integer} row integer - */ - GridEditor.findUpperCellWidthRowspanAndDecreaseByOne = function(col, row) { - var upperCell = GridEditor.getCell(col, row - 1); - if (!upperCell) { - return false; - } - - if (upperCell.spanned === 1) { - GridEditor.findUpperCellWidthRowspanAndDecreaseByOne(col, row - 1); - } else { - if (upperCell.rowspan > 1) { - GridEditor.removeRowspan(col, row - 1); - } - } - }; - - /** - * Removes the outermost right column from the grid. - */ - GridEditor.removeColumn = function() { - if (GridEditor.colCount <= 1) { - return false; - } - var newData = []; - - for (var rowIndex = 0; rowIndex < GridEditor.rowCount; rowIndex++) { - var newRow = []; - for (var colIndex = 0; colIndex < GridEditor.colCount - 1; colIndex++) { - newRow.push(GridEditor.data[rowIndex][colIndex]); - } - if (GridEditor.data[rowIndex][GridEditor.colCount - 1].spanned === 1) { - GridEditor.findLeftCellWidthColspanAndDecreaseByOne(GridEditor.colCount - 1, rowIndex); - } - newData.push(newRow); - } - - GridEditor.data = newData; - GridEditor.colCount--; - }; - - /** - * Checks if there are any cells on the left side of a given cell with a - * rowspan that spans over the given cell. - * - * @param {Integer} col - * @param {Integer} row - */ - GridEditor.findLeftCellWidthColspanAndDecreaseByOne = function(col, row) { - var leftCell = GridEditor.getCell(col - 1, row); - if (!leftCell) { - return false; - } - - if (leftCell.spanned === 1) { - GridEditor.findLeftCellWidthColspanAndDecreaseByOne(col - 1, row); - } else { - if (leftCell.colspan > 1) { - GridEditor.removeColspan(col - 1, row); - } - } - }; - - /** - * Adds a column at the right side of the grid. - */ - GridEditor.addColumn = function() { - for (var rowIndex = 0; rowIndex < GridEditor.rowCount; rowIndex++) { - var newCell = GridEditor.getNewCell(); - newCell.name = GridEditor.colCount + 'x' + rowIndex; - GridEditor.data[rowIndex].push(newCell); - } - GridEditor.colCount++; - }; - - /** - * Draws the grid as table into a given container. - * It also adds all needed links and bindings to the cells to make it editable. - */ - GridEditor.drawTable = function() { - var col; - var $colgroup = $('<colgroup>'); - for (col = 0; col < GridEditor.colCount; col++) { - $colgroup.append($('<col>').css({ - width: parseInt(100 / GridEditor.colCount, 10) + '%' - })); - } - var $table = $('<table id="base" class="table editor">'); - $table.append($colgroup); - - for (var row = 0; row < GridEditor.rowCount; row++) { - var rowData = GridEditor.data[row]; - if (rowData.length === 0) { - continue; - } - - var $row = $('<tr>'); - - for (col = 0; col < GridEditor.colCount; col++) { - var cell = GridEditor.data[row][col]; - if (cell.spanned === 1) { - continue; - } - var $cell = $('<td>').css({ - height: parseInt(100 / GridEditor.rowCount, 10) * cell.rowspan + '%', - width: parseInt(100 / GridEditor.colCount, 10) * cell.colspan + '%' - }); - var $container = $('<div class="cell_container">'); - $cell.append($container); - var $anchor = $('<a href="#" data-col="' + col + '" data-row="' + row + '">'); - - $container.append( - $anchor - .clone() - .attr('class', 't3js-grideditor-link-editor link link_editor') - .attr('title', TYPO3.lang['grid_editCell']) - ); - if (GridEditor.cellCanSpanRight(col, row)) { - $container.append( - $anchor - .clone() - .attr('class', 't3js-grideditor-link-expand-right link link_expand_right') - .attr('title', TYPO3.lang['grid_mergeCell']) - ); - } - if (GridEditor.cellCanShrinkLeft(col, row)) { - $container.append( - $anchor - .clone() - .attr('class', 't3js-grideditor-link-shrink-left link link_shrink_left') - .attr('title', TYPO3.lang['grid_splitCell']) - ); - } - if (GridEditor.cellCanSpanDown(col, row)) { - $container.append( - $anchor - .clone() - .attr('class', 't3js-grideditor-link-expand-down link link_expand_down') - .attr('title', TYPO3.lang['grid_mergeCell']) - ); - } - if (GridEditor.cellCanShrinkUp(col, row)) { - $container.append( - $anchor - .clone() - .attr('class', 't3js-grideditor-link-shrink-up link link_shrink_up') - .attr('title', TYPO3.lang['grid_splitCell']) - ); - } - $cell.append( - $('<div class="cell_data">') - .html( - TYPO3.lang['grid_name'] + ': ' - + (cell.name ? GridEditor.stripMarkup(cell.name) : TYPO3.lang['grid_notSet']) - + '<br />' - + TYPO3.lang['grid_column'] + ': ' - + (cell.column === undefined ? TYPO3.lang['grid_notSet'] : parseInt(cell.column, 10)) - ) - ); - if (cell.colspan > 1) { - $cell.attr('colspan', cell.colspan); - } - if (cell.rowspan > 1) { - $cell.attr('rowspan', cell.rowspan); - } - $row.append($cell); - } - $table.append($row); - } - $(GridEditor.targetElement).empty().append($table); - }; - - /** - * Sets the name of a certain grid element. - * - * @param {String} newName - * @param {Integer} col - * @param {Integer} row - * - * @returns {Boolean} - */ - GridEditor.setName = function(newName, col, row) { - var cell = GridEditor.getCell(col, row); - if (!cell) { - return false; - } - cell.name = GridEditor.stripMarkup(newName); - return true; - }; - - /** - * Sets the column field for a certain grid element. This is NOT the column of the - * element itself. - * - * @param {Integer} newColumn - * @param {Integer} col - * @param {Integer} row - * - * @returns {Boolean} - */ - GridEditor.setColumn = function(newColumn, col, row) { - var cell = GridEditor.getCell(col, row); - if (!cell) { - return false; - } - cell.column = parseInt(newColumn, 10); - return true; - }; - - /** - * Creates an ExtJs Window with two input fields and shows it. On save, the data - * is written into the grid element. - * - * @param {Integer} col - * @param {Integer} row - * - * @returns {Boolean} - */ - GridEditor.showOptions = function(col, row) { - var cell = GridEditor.getCell(col, row); - if (!cell) { - return false; - } - - var colPos; - if (cell.column === 0) { - colPos = 0; - } else if(parseInt(cell.column, 10)) { - colPos = parseInt(cell.column, 10); - } else { - colPos = ''; - } - - var $markup = $('<div>'); - var $formGroup = $('<div class="form-group">'); - var $label = $('<label>'); - var $input = $('<input>'); - - $markup.append([ - $formGroup - .clone() - .append([ - $label - .clone() - .text(TYPO3.lang['grid_nameHelp']) - , - $input - .clone() - .attr('type', 'text') - .attr('class', 't3js-grideditor-field-name form-control') - .attr('name', 'name') - .val(GridEditor.stripMarkup(cell.name) || '') - ]), - $formGroup - .clone() - .append([ - $label - .clone() - .text(TYPO3.lang['grid_columnHelp']) - , - $input - .clone() - .attr('type', 'text') - .attr('class', 't3js-grideditor-field-colpos form-control') - .attr('name', 'column') - .val(colPos) - ]) - ]); - - var $modal = Modal.show(TYPO3.lang['grid_windowTitle'], $markup, Severity.notice, [ - { - text: $(this).data('button-close-text') || TYPO3.lang['button.cancel'] || 'Cancel', - active: true, - btnClass: 'btn-default', - name: 'cancel' - }, - { - text: $(this).data('button-ok-text') || TYPO3.lang['button.ok'] || 'OK', - btnClass: 'btn-' + Severity.getCssClass(Severity.notice), - name: 'ok' - } - ]); - $modal.data('col', col); - $modal.data('row', row); - $modal.on('button.clicked', function(e) { - if (e.target.name === 'cancel') { - Modal.currentModal.trigger('modal-dismiss'); - } else if (e.target.name === 'ok') { - GridEditor.setName($modal.find('.t3js-grideditor-field-name').val(), $modal.data('col'), $modal.data('row')); - GridEditor.setColumn($modal.find('.t3js-grideditor-field-colpos').val(), $modal.data('col'), $modal.data('row')); - GridEditor.drawTable(); - GridEditor.writeConfig(GridEditor.export2LayoutRecord()); - Modal.currentModal.trigger('modal-dismiss'); - } - }); - }; - - /** - * Returns a cell element from the grid. - * - * @param {Integer} col - * @param {Integer} row - * @returns {Object} - */ - GridEditor.getCell = function(col, row) { - if (col > GridEditor.colCount - 1) { - return false; - } - if (row > GridEditor.rowCount - 1) { - return false; - } - if (GridEditor.data.length > row-1 && GridEditor.data[row].length > col-1) { - return GridEditor.data[row][col]; - } - return false; - }; - - /** - * Checks whether a cell can span to the right or not. A cell can span to the right - * if it is not in the last column and if there is no cell beside it that is - * already overspanned by some other cell. - * - * @param {Integer} col - * @param {Integer} row - * @returns {Boolean} - */ - GridEditor.cellCanSpanRight = function(col, row) { - if (col == GridEditor.colCount - 1) { - return false; - } - - var cell = GridEditor.getCell(col, row); - var checkCell; - if (cell.rowspan > 1) { - for (var rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) { - checkCell = GridEditor.getCell(col + cell.colspan, rowIndex); - if (!checkCell || checkCell.spanned === 1 || checkCell.colspan > 1 || checkCell.rowspan > 1) { - return false; - } - } - } else { - checkCell = GridEditor.getCell(col + cell.colspan, row); - if (!checkCell || cell.spanned === 1 || checkCell.spanned === 1 || checkCell.colspan > 1 || checkCell.rowspan > 1) { - return false; - } - } - - return true; - }; - - /** - * Checks whether a cell can span down or not. - * - * @param {Integer} col - * @param {Integer} row - * @returns {Boolean} - */ - GridEditor.cellCanSpanDown = function(col, row) { - if (row == GridEditor.rowCount - 1) { - return false; - } - - var cell = GridEditor.getCell(col, row); - var checkCell; - if (cell.colspan > 1) { - // we have to check all cells on the right side for the complete colspan - for (var colIndex = col; colIndex < col + cell.colspan; colIndex++) { - checkCell = GridEditor.getCell(colIndex, row + cell.rowspan); - if (!checkCell || checkCell.spanned === 1 || checkCell.colspan > 1 || checkCell.rowspan > 1) { - return false; - } - } - } else { - checkCell = GridEditor.getCell(col, row + cell.rowspan); - if (!checkCell || cell.spanned === 1 || checkCell.spanned === 1 || checkCell.colspan > 1 || checkCell.rowspan > 1) { - return false; - } - } - - return true; - }; - - /** - * Checks if a cell can shrink to the left. It can shrink if the colspan of the - * cell is bigger than 1. - * - * @param {Integer} col - * @param {Integer} row - * @returns {Boolean} - */ - GridEditor.cellCanShrinkLeft = function(col, row) { - return (GridEditor.data[row][col].colspan > 1); - }; - - /** - * Returns if a cell can shrink up. This is the case if a cell has at least - * a rowspan of 2. - * - * @param {Integer} col - * @param {Integer} row - * @returns {Boolean} - */ - GridEditor.cellCanShrinkUp = function(col, row) { - return (GridEditor.data[row][col].rowspan > 1); - }; - - /** - * Adds a colspan to a grid element. - * - * @param {Integer} col - * @param {Integer} row - * @returns {Boolean} - */ - GridEditor.addColspan = function(col, row) { - var cell = GridEditor.getCell(col, row); - if (!cell || !GridEditor.cellCanSpanRight(col, row)) { - return false; - } - - for (var rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) { - GridEditor.data[rowIndex][col + cell.colspan].spanned = 1; - } - cell.colspan += 1; - }; - - /** - * Adds a rowspan to grid element. - * - * @param {Integer} col - * @param {Integer} row - * @returns {Boolean} - */ - GridEditor.addRowspan = function(col, row) { - var cell = GridEditor.getCell(col, row); - if (!cell || !GridEditor.cellCanSpanDown(col, row)) { - return false; - } - - for (var colIndex = col; colIndex < col + cell.colspan; colIndex++) { - GridEditor.data[row + cell.rowspan][colIndex].spanned = 1; - } - cell.rowspan += 1; - }; - - /** - * Removes a colspan from a grid element. - * - * @param {Integer} col - * @param {Integer} row - * @returns {Boolean} - */ - GridEditor.removeColspan = function(col, row) { - var cell = GridEditor.getCell(col, row); - if (!cell || !GridEditor.cellCanShrinkLeft(col, row)) { - return false; - } - - cell.colspan -= 1; - - for (var rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) { - GridEditor.data[rowIndex][col + cell.colspan].spanned = 0; - } - }; - - /** - * Removes a rowspan from a grid element. - * - * @param {Integer} col - * @param {Integer} row - * @returns {Boolean} - */ - GridEditor.removeRowspan = function(col, row) { - var cell = GridEditor.getCell(col, row); - if (!cell || !GridEditor.cellCanShrinkUp(col, row)) { - return false; - } - - cell.rowspan -= 1; - for (var colIndex = col; colIndex < col + cell.colspan; colIndex++) { - GridEditor.data[row + cell.rowspan][colIndex].spanned = 0; - } - }; - - /** - * Exports the current grid to a TypoScript notation that can be read by the - * page module and is human readable. - * - * @returns {String} +var __values = (this && this.__values) || function (o) { + var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0; + if (m) return m.call(o); + return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; +}; +define(["require", "exports", "TYPO3/CMS/Backend/Modal", "TYPO3/CMS/Backend/Severity", "jquery", "bootstrap"], function (require, exports, Modal, Severity, $) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + /** + * Module: TYPO3/CMS/Backend/GridEditor + * @exports TYPO3/CMS/Backend/GridEditor */ - GridEditor.export2LayoutRecord = function() { - var result = "backend_layout {\n\tcolCount = " + GridEditor.colCount + "\n\trowCount = " + GridEditor.rowCount + "\n\trows {\n"; - for (var row = 0; row < GridEditor.rowCount; row++) { - result += "\t\t" + (row + 1) + " {\n"; - result += "\t\t\tcolumns {\n"; - var colIndex = 0; - for (var col = 0; col < GridEditor.colCount; col++) { - var cell = GridEditor.getCell(col, row); - if (cell && !cell.spanned) { - colIndex++; - result += "\t\t\t\t" + (colIndex) + " {\n"; - result += "\t\t\t\t\tname = " + ((!cell.name) ? col + "x" + row : cell.name) + "\n"; - if (cell.colspan > 1) { - result += "\t\t\t\t\tcolspan = " + cell.colspan + "\n"; - } - if (cell.rowspan > 1) { - result += "\t\t\t\t\trowspan = " + cell.rowspan + "\n"; - } - if (typeof(cell.column) === 'number') { - result += "\t\t\t\t\tcolPos = " + cell.column + "\n"; - } - result += "\t\t\t\t}\n"; - } - - } - result += "\t\t\t}\n"; - result += "\t\t}\n"; - } - - result += "\t}\n}\n"; - return result; - }; - - /** - * Remove all markup - * - * @param {String} input - * @returns {*|jQuery} - */ - GridEditor.stripMarkup = function(input) { - input = input.replace( /<(.*)>/gi, ''); - return $('<p>' + input + '</p>').text(); - }; - - GridEditor.initialize(); - return GridEditor; + var GridEditor = (function () { + /** + * + * @param {GridEditorConfigurationInterface} config + */ + function GridEditor(config) { + if (config === void 0) { config = null; } + var _this = this; + this.colCount = 1; + this.rowCount = 1; + this.nameLabel = 'name'; + this.columnLabel = 'columen label'; + this.defaultCell = { spanned: 0, rowspan: 1, colspan: 1, name: '', colpos: '', column: undefined }; + this.selectorEditor = '.t3js-grideditor'; + this.selectorAddColumn = '.t3js-grideditor-addcolumn'; + this.selectorRemoveColumn = '.t3js-grideditor-removecolumn'; + this.selectorAddRowTop = '.t3js-grideditor-addrow-top'; + this.selectorRemoveRowTop = '.t3js-grideditor-removerow-top'; + this.selectorAddRowBottom = '.t3js-grideditor-addrow-bottom'; + this.selectorRemoveRowBottom = '.t3js-grideditor-removerow-bottom'; + this.selectorLinkEditor = '.t3js-grideditor-link-editor'; + this.selectorLinkExpandRight = '.t3js-grideditor-link-expand-right'; + this.selectorLinkShrinkLeft = '.t3js-grideditor-link-shrink-left'; + this.selectorLinkExpandDown = '.t3js-grideditor-link-expand-down'; + this.selectorLinkShrinkUp = '.t3js-grideditor-link-shrink-up'; + this.selectorDocHeaderSave = '.t3js-grideditor-savedok'; + this.selectorDocHeaderSaveClose = '.t3js-grideditor-savedokclose'; + this.selectorConfigPreview = '.t3js-grideditor-preview-config'; + this.selectorConfigPreviewButton = '.t3js-grideditor-preview-button'; + /** + * + * @param {Event} e + */ + this.modalButtonClickHandler = function (e) { + var button = e.target; + if (button.name === 'cancel') { + Modal.currentModal.trigger('modal-dismiss'); + } + else if (button.name === 'ok') { + _this.setName(Modal.currentModal.find('.t3js-grideditor-field-name').val(), Modal.currentModal.data('col'), Modal.currentModal.data('row')); + _this.setColumn(Modal.currentModal.find('.t3js-grideditor-field-colpos').val(), Modal.currentModal.data('col'), Modal.currentModal.data('row')); + _this.drawTable(); + _this.writeConfig(_this.export2LayoutRecord()); + Modal.currentModal.trigger('modal-dismiss'); + } + }; + /** + * + * @param {Event} e + */ + this.addColumnHandler = function (e) { + e.preventDefault(); + _this.addColumn(); + _this.drawTable(); + _this.writeConfig(_this.export2LayoutRecord()); + }; + /** + * + * @param {Event} e + */ + this.removeColumnHandler = function (e) { + e.preventDefault(); + _this.removeColumn(); + _this.drawTable(); + _this.writeConfig(_this.export2LayoutRecord()); + }; + /** + * + * @param {Event} e + */ + this.addRowTopHandler = function (e) { + e.preventDefault(); + _this.addRowTop(); + _this.drawTable(); + _this.writeConfig(_this.export2LayoutRecord()); + }; + /** + * + * @param {Event} e + */ + this.addRowBottomHandler = function (e) { + e.preventDefault(); + _this.addRowBottom(); + _this.drawTable(); + _this.writeConfig(_this.export2LayoutRecord()); + }; + /** + * + * @param {Event} e + */ + this.removeRowTopHandler = function (e) { + e.preventDefault(); + _this.removeRowTop(); + _this.drawTable(); + _this.writeConfig(_this.export2LayoutRecord()); + }; + /** + * + * @param {Event} e + */ + this.removeRowBottomHandler = function (e) { + e.preventDefault(); + _this.removeRowBottom(); + _this.drawTable(); + _this.writeConfig(_this.export2LayoutRecord()); + }; + /** + * + * @param {Event} e + */ + this.linkEditorHandler = function (e) { + e.preventDefault(); + var $element = $(e.target); + _this.showOptions($element.data('col'), $element.data('row')); + }; + /** + * + * @param {Event} e + */ + this.linkExpandRightHandler = function (e) { + e.preventDefault(); + var $element = $(e.target); + _this.addColspan($element.data('col'), $element.data('row')); + _this.drawTable(); + _this.writeConfig(_this.export2LayoutRecord()); + }; + /** + * + * @param {Event} e + */ + this.linkShrinkLeftHandler = function (e) { + e.preventDefault(); + var $element = $(e.target); + _this.removeColspan($element.data('col'), $element.data('row')); + _this.drawTable(); + _this.writeConfig(_this.export2LayoutRecord()); + }; + /** + * + * @param {Event} e + */ + this.linkExpandDownHandler = function (e) { + e.preventDefault(); + var $element = $(e.target); + _this.addRowspan($element.data('col'), $element.data('row')); + _this.drawTable(); + _this.writeConfig(_this.export2LayoutRecord()); + }; + /** + * + * @param {Event} e + */ + this.linkShrinkUpHandler = function (e) { + e.preventDefault(); + var $element = $(e.target); + _this.removeRowspan($element.data('col'), $element.data('row')); + _this.drawTable(); + _this.writeConfig(_this.export2LayoutRecord()); + }; + /** + * + * @param {Event} e + */ + this.configPreviewButtonHandler = function (e) { + e.preventDefault(); + var $preview = $(_this.selectorConfigPreview); + var $button = $(_this.selectorConfigPreviewButton); + if ($preview.is(':visible')) { + $button.empty().append(TYPO3.lang['button.showPageTsConfig']); + $(_this.selectorConfigPreview).slideUp(); + } + else { + $button.empty().append(TYPO3.lang['button.hidePageTsConfig']); + $(_this.selectorConfigPreview).slideDown(); + } + }; + var $element = $(this.selectorEditor); + this.colCount = $element.data('colcount'); + this.rowCount = $element.data('rowcount'); + this.field = $('input[name="' + $element.data('field') + '"]'); + this.data = $element.data('data'); + this.nameLabel = config !== null ? config.nameLabel : 'Name'; + this.columnLabel = config !== null ? config.columnLabel : 'Column'; + this.targetElement = $(this.selectorEditor); + $(this.selectorConfigPreview).hide(); + $(this.selectorConfigPreviewButton).empty().append(TYPO3.lang['button.showPageTsConfig']); + this.initializeEvents(); + this.drawTable(); + this.writeConfig(this.export2LayoutRecord()); + } + /** + * Remove all markup + * + * @param {String} input + * @returns {string} + */ + GridEditor.stripMarkup = function (input) { + input = input.replace(/<(.*)>/gi, ''); + return $('<p>' + input + '</p>').text(); + }; + /** + * + */ + GridEditor.prototype.initializeEvents = function () { + $(document).on('click', this.selectorAddColumn, this.addColumnHandler); + $(document).on('click', this.selectorRemoveColumn, this.removeColumnHandler); + $(document).on('click', this.selectorAddRowTop, this.addRowTopHandler); + $(document).on('click', this.selectorAddRowBottom, this.addRowBottomHandler); + $(document).on('click', this.selectorRemoveRowTop, this.removeRowTopHandler); + $(document).on('click', this.selectorRemoveRowBottom, this.removeRowBottomHandler); + $(document).on('click', this.selectorLinkEditor, this.linkEditorHandler); + $(document).on('click', this.selectorLinkExpandRight, this.linkExpandRightHandler); + $(document).on('click', this.selectorLinkShrinkLeft, this.linkShrinkLeftHandler); + $(document).on('click', this.selectorLinkExpandDown, this.linkExpandDownHandler); + $(document).on('click', this.selectorLinkShrinkUp, this.linkShrinkUpHandler); + $(document).on('click', this.selectorConfigPreviewButton, this.configPreviewButtonHandler); + }; + /** + * Create a new cell from defaultCell + * @returns {Object} + */ + GridEditor.prototype.getNewCell = function () { + return $.extend({}, this.defaultCell); + }; + /** + * write data back to hidden field + * + * @param data + */ + GridEditor.prototype.writeConfig = function (data) { + this.field.val(data); + var configLines = data.split('\n'); + var config = ''; + try { + for (var configLines_1 = __values(configLines), configLines_1_1 = configLines_1.next(); !configLines_1_1.done; configLines_1_1 = configLines_1.next()) { + var line = configLines_1_1.value; + if (line) { + config += '\t\t\t' + line + '\n'; + } + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (configLines_1_1 && !configLines_1_1.done && (_a = configLines_1.return)) _a.call(configLines_1); + } + finally { if (e_1) throw e_1.error; } + } + $(this.selectorConfigPreview).find('code').empty().append('mod.web_layout.BackendLayouts {\n' + + ' exampleKey {\n' + + ' title = Example\n' + + ' icon = EXT:example_extension/Resources/Public/Images/BackendLayouts/default.gif\n' + + ' config {\n' + + config.replace(new RegExp('\t', 'g'), ' ') + + ' }\n' + + ' }\n' + + '}\n'); + var e_1, _a; + }; + /** + * Add a new row at the top + */ + GridEditor.prototype.addRowTop = function () { + var newRow = []; + for (var i = 0; i < this.colCount; i++) { + var newCell = this.getNewCell(); + newCell.name = i + 'x' + this.data.length; + newRow[i] = newCell; + } + this.data.unshift(newRow); + this.rowCount++; + }; + /** + * Add a new row at the bottom + */ + GridEditor.prototype.addRowBottom = function () { + var newRow = []; + for (var i = 0; i < this.colCount; i++) { + var newCell = this.getNewCell(); + newCell.name = i + 'x' + this.data.length; + newRow[i] = newCell; + } + this.data.push(newRow); + this.rowCount++; + }; + /** + * Removes the first row of the grid and adjusts all cells that might be effected + * by that change. (Removing colspans) + */ + GridEditor.prototype.removeRowTop = function () { + if (this.rowCount <= 1) { + return false; + } + var newData = []; + for (var rowIndex = 1; rowIndex < this.rowCount; rowIndex++) { + newData.push(this.data[rowIndex]); + } + // fix rowspan in former last row + for (var colIndex = 0; colIndex < this.colCount; colIndex++) { + if (this.data[0][colIndex].spanned === 1) { + this.findUpperCellWidthRowspanAndDecreaseByOne(colIndex, 0); + } + } + this.data = newData; + this.rowCount--; + return true; + }; + /** + * Removes the last row of the grid and adjusts all cells that might be effected + * by that change. (Removing colspans) + */ + GridEditor.prototype.removeRowBottom = function () { + if (this.rowCount <= 1) { + return false; + } + var newData = []; + for (var rowIndex = 0; rowIndex < this.rowCount - 1; rowIndex++) { + newData.push(this.data[rowIndex]); + } + // fix rowspan in former last row + for (var colIndex = 0; colIndex < this.colCount; colIndex++) { + if (this.data[this.rowCount - 1][colIndex].spanned === 1) { + this.findUpperCellWidthRowspanAndDecreaseByOne(colIndex, this.rowCount - 1); + } + } + this.data = newData; + this.rowCount--; + return true; + }; + /** + * Takes a cell and looks above it if there are any cells that have colspans that + * spans into the given cell. This is used when a row was removed from the grid + * to make sure that no cell with wrong colspans exists in the grid. + * + * @param {number} col + * @param {number} row integer + */ + GridEditor.prototype.findUpperCellWidthRowspanAndDecreaseByOne = function (col, row) { + var upperCell = this.getCell(col, row - 1); + if (!upperCell) { + return false; + } + if (upperCell.spanned === 1) { + this.findUpperCellWidthRowspanAndDecreaseByOne(col, row - 1); + } + else { + if (upperCell.rowspan > 1) { + this.removeRowspan(col, row - 1); + } + } + return true; + }; + /** + * Removes the outermost right column from the grid. + */ + GridEditor.prototype.removeColumn = function () { + if (this.colCount <= 1) { + return false; + } + var newData = []; + for (var rowIndex = 0; rowIndex < this.rowCount; rowIndex++) { + var newRow = []; + for (var colIndex = 0; colIndex < this.colCount - 1; colIndex++) { + newRow.push(this.data[rowIndex][colIndex]); + } + if (this.data[rowIndex][this.colCount - 1].spanned === 1) { + this.findLeftCellWidthColspanAndDecreaseByOne(this.colCount - 1, rowIndex); + } + newData.push(newRow); + } + this.data = newData; + this.colCount--; + return true; + }; + /** + * Checks if there are any cells on the left side of a given cell with a + * rowspan that spans over the given cell. + * + * @param {number} col + * @param {number} row + */ + GridEditor.prototype.findLeftCellWidthColspanAndDecreaseByOne = function (col, row) { + var leftCell = this.getCell(col - 1, row); + if (!leftCell) { + return false; + } + if (leftCell.spanned === 1) { + this.findLeftCellWidthColspanAndDecreaseByOne(col - 1, row); + } + else { + if (leftCell.colspan > 1) { + this.removeColspan(col - 1, row); + } + } + return true; + }; + /** + * Adds a column at the right side of the grid. + */ + GridEditor.prototype.addColumn = function () { + for (var rowIndex = 0; rowIndex < this.rowCount; rowIndex++) { + var newCell = this.getNewCell(); + newCell.name = this.colCount + 'x' + rowIndex; + this.data[rowIndex].push(newCell); + } + this.colCount++; + }; + /** + * Draws the grid as table into a given container. + * It also adds all needed links and bindings to the cells to make it editable. + */ + GridEditor.prototype.drawTable = function () { + var $colgroup = $('<colgroup>'); + for (var col = 0; col < this.colCount; col++) { + var percent = 100 / this.colCount; + $colgroup.append($('<col>').css({ + width: parseInt(percent.toString(), 10) + '%', + })); + } + var $table = $('<table id="base" class="table editor">'); + $table.append($colgroup); + for (var row = 0; row < this.rowCount; row++) { + var rowData = this.data[row]; + if (rowData.length === 0) { + continue; + } + var $row = $('<tr>'); + for (var col = 0; col < this.colCount; col++) { + var cell = this.data[row][col]; + if (cell.spanned === 1) { + continue; + } + var percentRow = 100 / this.rowCount; + var percentCol = 100 / this.colCount; + var $cell = $('<td>').css({ + height: parseInt(percentRow.toString(), 10) * cell.rowspan + '%', + width: parseInt(percentCol.toString(), 10) * cell.colspan + '%', + }); + var $container = $('<div class="cell_container">'); + $cell.append($container); + var $anchor = $('<a href="#" data-col="' + col + '" data-row="' + row + '">'); + $container.append($anchor + .clone() + .attr('class', 't3js-grideditor-link-editor link link_editor') + .attr('title', TYPO3.lang.grid_editCell)); + if (this.cellCanSpanRight(col, row)) { + $container.append($anchor + .clone() + .attr('class', 't3js-grideditor-link-expand-right link link_expand_right') + .attr('title', TYPO3.lang.grid_mergeCell)); + } + if (this.cellCanShrinkLeft(col, row)) { + $container.append($anchor + .clone() + .attr('class', 't3js-grideditor-link-shrink-left link link_shrink_left') + .attr('title', TYPO3.lang.grid_splitCell)); + } + if (this.cellCanSpanDown(col, row)) { + $container.append($anchor + .clone() + .attr('class', 't3js-grideditor-link-expand-down link link_expand_down') + .attr('title', TYPO3.lang.grid_mergeCell)); + } + if (this.cellCanShrinkUp(col, row)) { + $container.append($anchor + .clone() + .attr('class', 't3js-grideditor-link-shrink-up link link_shrink_up') + .attr('title', TYPO3.lang.grid_splitCell)); + } + $cell.append($('<div class="cell_data">') + .html(TYPO3.lang.grid_name + ': ' + + (cell.name ? GridEditor.stripMarkup(cell.name) : TYPO3.lang.grid_notSet) + + '<br />' + + TYPO3.lang.grid_column + ': ' + + (typeof cell.column === 'undefined' || isNaN(cell.column) + ? TYPO3.lang.grid_notSet + : parseInt(cell.column, 10)))); + if (cell.colspan > 1) { + $cell.attr('colspan', cell.colspan); + } + if (cell.rowspan > 1) { + $cell.attr('rowspan', cell.rowspan); + } + $row.append($cell); + } + $table.append($row); + } + $(this.targetElement).empty().append($table); + }; + /** + * Sets the name of a certain grid element. + * + * @param {String} newName + * @param {number} col + * @param {number} row + * + * @returns {Boolean} + */ + GridEditor.prototype.setName = function (newName, col, row) { + var cell = this.getCell(col, row); + if (!cell) { + return false; + } + cell.name = GridEditor.stripMarkup(newName); + return true; + }; + /** + * Sets the column field for a certain grid element. This is NOT the column of the + * element itself. + * + * @param {number} newColumn + * @param {number} col + * @param {number} row + * + * @returns {Boolean} + */ + GridEditor.prototype.setColumn = function (newColumn, col, row) { + var cell = this.getCell(col, row); + if (!cell) { + return false; + } + cell.column = parseInt(newColumn.toString(), 10); + return true; + }; + /** + * Creates an ExtJs Window with two input fields and shows it. On save, the data + * is written into the grid element. + * + * @param {number} col + * @param {number} row + * + * @returns {Boolean} + */ + GridEditor.prototype.showOptions = function (col, row) { + var cell = this.getCell(col, row); + if (!cell) { + return false; + } + var colPos; + if (cell.column === 0) { + colPos = 0; + } + else if (cell.column) { + colPos = parseInt(cell.column.toString(), 10); + } + else { + colPos = ''; + } + var $markup = $('<div>'); + var $formGroup = $('<div class="form-group">'); + var $label = $('<label>'); + var $input = $('<input>'); + $markup.append([ + $formGroup + .clone() + .append([ + $label + .clone() + .text(TYPO3.lang.grid_nameHelp), + $input + .clone() + .attr('type', 'text') + .attr('class', 't3js-grideditor-field-name form-control') + .attr('name', 'name') + .val(GridEditor.stripMarkup(cell.name) || ''), + ]), + $formGroup + .clone() + .append([ + $label + .clone() + .text(TYPO3.lang.grid_columnHelp), + $input + .clone() + .attr('type', 'text') + .attr('class', 't3js-grideditor-field-colpos form-control') + .attr('name', 'column') + .val(colPos), + ]), + ]); + var $modal = Modal.show(TYPO3.lang.grid_windowTitle, $markup, Severity.notice, [ + { + active: true, + btnClass: 'btn-default', + name: 'cancel', + text: $(this).data('button-close-text') || TYPO3.lang['button.cancel'] || 'Cancel', + }, + { + btnClass: 'btn-' + Severity.getCssClass(Severity.notice), + name: 'ok', + text: $(this).data('button-ok-text') || TYPO3.lang['button.ok'] || 'OK', + }, + ]); + $modal.data('col', col); + $modal.data('row', row); + $modal.on('button.clicked', this.modalButtonClickHandler); + return true; + }; + /** + * Returns a cell element from the grid. + * + * @param {number} col + * @param {number} row + */ + GridEditor.prototype.getCell = function (col, row) { + if (col > this.colCount - 1) { + return false; + } + if (row > this.rowCount - 1) { + return false; + } + if (this.data.length > row - 1 && this.data[row].length > col - 1) { + return this.data[row][col]; + } + return null; + }; + /** + * Checks whether a cell can span to the right or not. A cell can span to the right + * if it is not in the last column and if there is no cell beside it that is + * already overspanned by some other cell. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + GridEditor.prototype.cellCanSpanRight = function (col, row) { + if (col === this.colCount - 1) { + return false; + } + var cell = this.getCell(col, row); + var checkCell; + if (cell.rowspan > 1) { + for (var rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) { + checkCell = this.getCell(col + cell.colspan, rowIndex); + if (!checkCell || checkCell.spanned === 1 || checkCell.colspan > 1 || checkCell.rowspan > 1) { + return false; + } + } + } + else { + checkCell = this.getCell(col + cell.colspan, row); + if (!checkCell || cell.spanned === 1 || checkCell.spanned === 1 || checkCell.colspan > 1 + || checkCell.rowspan > 1) { + return false; + } + } + return true; + }; + /** + * Checks whether a cell can span down or not. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + GridEditor.prototype.cellCanSpanDown = function (col, row) { + if (row === this.rowCount - 1) { + return false; + } + var cell = this.getCell(col, row); + var checkCell; + if (cell.colspan > 1) { + // we have to check all cells on the right side for the complete colspan + for (var colIndex = col; colIndex < col + cell.colspan; colIndex++) { + checkCell = this.getCell(colIndex, row + cell.rowspan); + if (!checkCell || checkCell.spanned === 1 || checkCell.colspan > 1 || checkCell.rowspan > 1) { + return false; + } + } + } + else { + checkCell = this.getCell(col, row + cell.rowspan); + if (!checkCell || cell.spanned === 1 || checkCell.spanned === 1 || checkCell.colspan > 1 + || checkCell.rowspan > 1) { + return false; + } + } + return true; + }; + /** + * Checks if a cell can shrink to the left. It can shrink if the colspan of the + * cell is bigger than 1. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + GridEditor.prototype.cellCanShrinkLeft = function (col, row) { + return (this.data[row][col].colspan > 1); + }; + /** + * Returns if a cell can shrink up. This is the case if a cell has at least + * a rowspan of 2. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + GridEditor.prototype.cellCanShrinkUp = function (col, row) { + return (this.data[row][col].rowspan > 1); + }; + /** + * Adds a colspan to a grid element. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + GridEditor.prototype.addColspan = function (col, row) { + var cell = this.getCell(col, row); + if (!cell || !this.cellCanSpanRight(col, row)) { + return false; + } + for (var rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) { + this.data[rowIndex][col + cell.colspan].spanned = 1; + } + cell.colspan += 1; + return true; + }; + /** + * Adds a rowspan to grid element. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + GridEditor.prototype.addRowspan = function (col, row) { + var cell = this.getCell(col, row); + if (!cell || !this.cellCanSpanDown(col, row)) { + return false; + } + for (var colIndex = col; colIndex < col + cell.colspan; colIndex++) { + this.data[row + cell.rowspan][colIndex].spanned = 1; + } + cell.rowspan += 1; + return true; + }; + /** + * Removes a colspan from a grid element. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + GridEditor.prototype.removeColspan = function (col, row) { + var cell = this.getCell(col, row); + if (!cell || !this.cellCanShrinkLeft(col, row)) { + return false; + } + cell.colspan -= 1; + for (var rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) { + this.data[rowIndex][col + cell.colspan].spanned = 0; + } + return true; + }; + /** + * Removes a rowspan from a grid element. + * + * @param {number} col + * @param {number} row + * @returns {Boolean} + */ + GridEditor.prototype.removeRowspan = function (col, row) { + var cell = this.getCell(col, row); + if (!cell || !this.cellCanShrinkUp(col, row)) { + return false; + } + cell.rowspan -= 1; + for (var colIndex = col; colIndex < col + cell.colspan; colIndex++) { + this.data[row + cell.rowspan][colIndex].spanned = 0; + } + return true; + }; + /** + * Exports the current grid to a TypoScript notation that can be read by the + * page module and is human readable. + * + * @returns {String} + */ + GridEditor.prototype.export2LayoutRecord = function () { + var result = 'backend_layout {\n\tcolCount = ' + this.colCount + '\n\trowCount = ' + this.rowCount + '\n\trows {\n'; + for (var row = 0; row < this.rowCount; row++) { + result += '\t\t' + (row + 1) + ' {\n'; + result += '\t\t\tcolumns {\n'; + var colIndex = 0; + for (var col = 0; col < this.colCount; col++) { + var cell = this.getCell(col, row); + if (cell) { + if (!cell.spanned) { + colIndex++; + result += '\t\t\t\t' + (colIndex) + ' {\n'; + result += '\t\t\t\t\tname = ' + ((!cell.name) ? col + 'x' + row : cell.name) + '\n'; + if (cell.colspan > 1) { + result += '\t\t\t\t\tcolspan = ' + cell.colspan + '\n'; + } + if (cell.rowspan > 1) { + result += '\t\t\t\t\trowspan = ' + cell.rowspan + '\n'; + } + if (typeof (cell.column) === 'number') { + result += '\t\t\t\t\tcolPos = ' + cell.column + '\n'; + } + result += '\t\t\t\t}\n'; + } + } + } + result += '\t\t\t}\n'; + result += '\t\t}\n'; + } + result += '\t}\n}\n'; + return result; + }; + return GridEditor; + }()); + exports.GridEditor = GridEditor; }); diff --git a/typo3/sysext/backend/Tests/JavaScript/GridEditorTest.js b/typo3/sysext/backend/Tests/JavaScript/GridEditorTest.js index 18321e646758..9b363381d5d3 100644 --- a/typo3/sysext/backend/Tests/JavaScript/GridEditorTest.js +++ b/typo3/sysext/backend/Tests/JavaScript/GridEditorTest.js @@ -1,45 +1,26 @@ -define(['jquery', 'TYPO3/CMS/Backend/GridEditor'], function($, GridEditor) { - 'use strict'; - - describe('TYPO3/CMS/Backend/GridEditorTest:', function() { - /** - * @test - */ - describe('tests for getNewCell', function() { - it('works and return a default cell object', function() { - var cell = { - spanned: 0, - rowspan: 1, - colspan: 1, - name: '', - colpos: '' - }; - expect(GridEditor.getNewCell()).toEqual(cell); - }); - }); - - /** - * @test - */ - describe('tests for addRow', function() { - var origData = GridEditor.data; - it('works and add a new row', function() { - //GridEditor.addRow(); - //expect(GridEditor.data.length).toBe(origData.length + 1); - pending('TypeError: undefined is not an object (evaluating GridEditor.data.push)'); - }); - }); - - /** - * @test - */ - describe('tests for stripMarkup', function() { - it('works with string which contains html markup only', function() { - expect(GridEditor.stripMarkup('<b>foo</b>')).toBe(''); - }); - it('works with string which contains html markup and normal text', function() { - expect(GridEditor.stripMarkup('<b>foo</b> bar')).toBe(' bar'); - }); - }); - }); +/* + * 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! + */ +define(["require", "exports", "TYPO3/CMS/Backend/GridEditor"], function (require, exports, GridEditor_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + describe('TYPO3/CMS/Backend/GridEditorTest:', function () { + describe('tests for stripMarkup', function () { + it('works with string which contains html markup only', function () { + expect(GridEditor_1.GridEditor.stripMarkup('<b>foo</b>')).toBe(''); + }); + it('works with string which contains html markup and normal text', function () { + expect(GridEditor_1.GridEditor.stripMarkup('<b>foo</b> bar')).toBe(' bar'); + }); + }); + }); }); diff --git a/typo3/sysext/backend/Tests/TypeScript/GridEditorTest.ts b/typo3/sysext/backend/Tests/TypeScript/GridEditorTest.ts new file mode 100644 index 000000000000..a822d9259fcf --- /dev/null +++ b/typo3/sysext/backend/Tests/TypeScript/GridEditorTest.ts @@ -0,0 +1,28 @@ +/* + * 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! + */ + +import $ = require('jquery'); +import {GridEditor} from 'TYPO3/CMS/Backend/GridEditor'; + +describe('TYPO3/CMS/Backend/GridEditorTest:', () => { + + describe('tests for stripMarkup', () => { + it('works with string which contains html markup only', () => { + expect(GridEditor.stripMarkup('<b>foo</b>')).toBe(''); + }); + it('works with string which contains html markup and normal text', () => { + expect(GridEditor.stripMarkup('<b>foo</b> bar')).toBe(' bar'); + }); + }); + +}); -- GitLab