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