[BUGFIX] Prevent loading jsfunc.inline.js twice
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Resources / Private / TypeScript / GridEditor.ts
1 /*
2  * This file is part of the TYPO3 CMS project.
3  *
4  * It is free software; you can redistribute it and/or modify it under
5  * the terms of the GNU General Public License, either version 2
6  * of the License, or any later version.
7  *
8  * For the full copyright and license information, please read the
9  * LICENSE.txt file that was distributed with this source code.
10  *
11  * The TYPO3 project - inspiring people to share!
12  */
13
14 import {SeverityEnum} from './Enum/Severity';
15 import 'bootstrap';
16 import * as $ from 'jquery';
17 import Modal = require('./Modal');
18
19 /**
20  * GridEditorConfigurationInterface
21  */
22 interface GridEditorConfigurationInterface {
23   nameLabel: string;
24   columnLabel: string;
25 }
26
27 /**
28  * CellInterface
29  */
30 interface CellInterface {
31   spanned: number;
32   rowspan: number;
33   colspan: number;
34   column: number;
35   name: string;
36   colpos: string;
37 }
38
39 /**
40  * Module: TYPO3/CMS/Backend/GridEditor
41  * @exports TYPO3/CMS/Backend/GridEditor
42  */
43 export class GridEditor {
44
45   protected colCount: number = 1;
46   protected rowCount: number = 1;
47   protected field: JQuery;
48   protected data: any[];
49   protected nameLabel: string = 'name';
50   protected columnLabel: string = 'columen label';
51   protected targetElement: JQuery;
52   protected defaultCell: object = {spanned: 0, rowspan: 1, colspan: 1, name: '', colpos: '', column: undefined};
53   protected selectorEditor: string = '.t3js-grideditor';
54   protected selectorAddColumn: string = '.t3js-grideditor-addcolumn';
55   protected selectorRemoveColumn: string = '.t3js-grideditor-removecolumn';
56   protected selectorAddRowTop: string = '.t3js-grideditor-addrow-top';
57   protected selectorRemoveRowTop: string = '.t3js-grideditor-removerow-top';
58   protected selectorAddRowBottom: string = '.t3js-grideditor-addrow-bottom';
59   protected selectorRemoveRowBottom: string = '.t3js-grideditor-removerow-bottom';
60   protected selectorLinkEditor: string = '.t3js-grideditor-link-editor';
61   protected selectorLinkExpandRight: string = '.t3js-grideditor-link-expand-right';
62   protected selectorLinkShrinkLeft: string = '.t3js-grideditor-link-shrink-left';
63   protected selectorLinkExpandDown: string = '.t3js-grideditor-link-expand-down';
64   protected selectorLinkShrinkUp: string = '.t3js-grideditor-link-shrink-up';
65   protected selectorDocHeaderSave: string = '.t3js-grideditor-savedok';
66   protected selectorDocHeaderSaveClose: string = '.t3js-grideditor-savedokclose';
67   protected selectorConfigPreview: string = '.t3js-grideditor-preview-config';
68   protected selectorConfigPreviewButton: string = '.t3js-grideditor-preview-button';
69
70   /**
71    * Remove all markup
72    *
73    * @param {String} input
74    * @returns {string}
75    */
76   public static stripMarkup(input: string): string {
77     input = input.replace(/<(.*)>/gi, '');
78     return $('<p>' + input + '</p>').text();
79   }
80
81   /**
82    *
83    * @param {GridEditorConfigurationInterface} config
84    */
85   constructor(config: GridEditorConfigurationInterface = null) {
86     const $element = $(this.selectorEditor);
87     this.colCount = $element.data('colcount');
88     this.rowCount = $element.data('rowcount');
89     this.field = $('input[name="' + $element.data('field') + '"]');
90     this.data = $element.data('data');
91     this.nameLabel = config !== null ? config.nameLabel : 'Name';
92     this.columnLabel = config !== null ? config.columnLabel : 'Column';
93     this.targetElement = $(this.selectorEditor);
94     $(this.selectorConfigPreview).hide();
95
96     $(this.selectorConfigPreviewButton).empty().append(TYPO3.lang['button.showPageTsConfig']);
97
98     this.initializeEvents();
99     this.drawTable();
100     this.writeConfig(this.export2LayoutRecord());
101   }
102
103   /**
104    *
105    */
106   protected initializeEvents(): void {
107     $(document).on('click', this.selectorAddColumn, this.addColumnHandler);
108     $(document).on('click', this.selectorRemoveColumn, this.removeColumnHandler);
109     $(document).on('click', this.selectorAddRowTop, this.addRowTopHandler);
110     $(document).on('click', this.selectorAddRowBottom, this.addRowBottomHandler);
111     $(document).on('click', this.selectorRemoveRowTop, this.removeRowTopHandler);
112     $(document).on('click', this.selectorRemoveRowBottom, this.removeRowBottomHandler);
113     $(document).on('click', this.selectorLinkEditor, this.linkEditorHandler);
114     $(document).on('click', this.selectorLinkExpandRight, this.linkExpandRightHandler);
115     $(document).on('click', this.selectorLinkShrinkLeft, this.linkShrinkLeftHandler);
116     $(document).on('click', this.selectorLinkExpandDown, this.linkExpandDownHandler);
117     $(document).on('click', this.selectorLinkShrinkUp, this.linkShrinkUpHandler);
118     $(document).on('click', this.selectorConfigPreviewButton, this.configPreviewButtonHandler);
119   }
120
121   /**
122    *
123    * @param {Event} e
124    */
125   protected modalButtonClickHandler = (e: Event) => {
126     const button: any = e.target;
127     if (button.name === 'cancel') {
128       Modal.currentModal.trigger('modal-dismiss');
129     } else if (button.name === 'ok') {
130       this.setName(
131         Modal.currentModal.find('.t3js-grideditor-field-name').val(),
132         Modal.currentModal.data('col'),
133         Modal.currentModal.data('row'),
134       );
135       this.setColumn(
136         Modal.currentModal.find('.t3js-grideditor-field-colpos').val(),
137         Modal.currentModal.data('col'),
138         Modal.currentModal.data('row'),
139       );
140       this.drawTable();
141       this.writeConfig(this.export2LayoutRecord());
142       Modal.currentModal.trigger('modal-dismiss');
143     }
144   }
145
146   /**
147    *
148    * @param {Event} e
149    */
150   protected addColumnHandler = (e: Event) => {
151     e.preventDefault();
152     this.addColumn();
153     this.drawTable();
154     this.writeConfig(this.export2LayoutRecord());
155   }
156
157   /**
158    *
159    * @param {Event} e
160    */
161   protected removeColumnHandler = (e: Event) => {
162     e.preventDefault();
163     this.removeColumn();
164     this.drawTable();
165     this.writeConfig(this.export2LayoutRecord());
166   }
167
168   /**
169    *
170    * @param {Event} e
171    */
172   protected addRowTopHandler = (e: Event) => {
173     e.preventDefault();
174     this.addRowTop();
175     this.drawTable();
176     this.writeConfig(this.export2LayoutRecord());
177   }
178
179   /**
180    *
181    * @param {Event} e
182    */
183   protected addRowBottomHandler = (e: Event) => {
184     e.preventDefault();
185     this.addRowBottom();
186     this.drawTable();
187     this.writeConfig(this.export2LayoutRecord());
188   }
189
190   /**
191    *
192    * @param {Event} e
193    */
194   protected removeRowTopHandler = (e: Event) => {
195     e.preventDefault();
196     this.removeRowTop();
197     this.drawTable();
198     this.writeConfig(this.export2LayoutRecord());
199   }
200
201   /**
202    *
203    * @param {Event} e
204    */
205   protected removeRowBottomHandler = (e: Event) => {
206     e.preventDefault();
207     this.removeRowBottom();
208     this.drawTable();
209     this.writeConfig(this.export2LayoutRecord());
210   }
211
212   /**
213    *
214    * @param {Event} e
215    */
216   protected linkEditorHandler = (e: Event) => {
217     e.preventDefault();
218     const $element = $(e.target);
219     this.showOptions($element.data('col'), $element.data('row'));
220   }
221
222   /**
223    *
224    * @param {Event} e
225    */
226   protected linkExpandRightHandler = (e: Event) => {
227     e.preventDefault();
228     const $element = $(e.target);
229     this.addColspan($element.data('col'), $element.data('row'));
230     this.drawTable();
231     this.writeConfig(this.export2LayoutRecord());
232   }
233
234   /**
235    *
236    * @param {Event} e
237    */
238   protected linkShrinkLeftHandler = (e: Event) => {
239     e.preventDefault();
240     const $element = $(e.target);
241     this.removeColspan($element.data('col'), $element.data('row'));
242     this.drawTable();
243     this.writeConfig(this.export2LayoutRecord());
244   }
245
246   /**
247    *
248    * @param {Event} e
249    */
250   protected linkExpandDownHandler = (e: Event) => {
251     e.preventDefault();
252     const $element = $(e.target);
253     this.addRowspan($element.data('col'), $element.data('row'));
254     this.drawTable();
255     this.writeConfig(this.export2LayoutRecord());
256   }
257
258   /**
259    *
260    * @param {Event} e
261    */
262   protected linkShrinkUpHandler = (e: Event) => {
263     e.preventDefault();
264     const $element = $(e.target);
265     this.removeRowspan($element.data('col'), $element.data('row'));
266     this.drawTable();
267     this.writeConfig(this.export2LayoutRecord());
268   }
269
270   /**
271    *
272    * @param {Event} e
273    */
274   protected configPreviewButtonHandler = (e: Event) => {
275     e.preventDefault();
276     const $preview = $(this.selectorConfigPreview);
277     const $button = $(this.selectorConfigPreviewButton);
278     if ($preview.is(':visible')) {
279       $button.empty().append(TYPO3.lang['button.showPageTsConfig']);
280       $(this.selectorConfigPreview).slideUp();
281     } else {
282       $button.empty().append(TYPO3.lang['button.hidePageTsConfig']);
283       $(this.selectorConfigPreview).slideDown();
284     }
285   }
286
287   /**
288    * Create a new cell from defaultCell
289    * @returns {Object}
290    */
291   protected getNewCell(): any {
292     return $.extend({}, this.defaultCell);
293   }
294
295   /**
296    * write data back to hidden field
297    *
298    * @param data
299    */
300   protected writeConfig(data: any): void {
301     this.field.val(data);
302     const configLines = data.split('\n');
303     let config = '';
304     for (const line of configLines) {
305       if (line) {
306         config += '\t\t\t' + line + '\n';
307       }
308     }
309     $(this.selectorConfigPreview).find('code').empty().append(
310       'mod.web_layout.BackendLayouts {\n' +
311       '  exampleKey {\n' +
312       '    title = Example\n' +
313       '    icon = EXT:example_extension/Resources/Public/Images/BackendLayouts/default.gif\n' +
314       '    config {\n' +
315       config.replace(new RegExp('\t', 'g'), '  ') +
316       '    }\n' +
317       '  }\n' +
318       '}\n',
319     );
320   }
321
322   /**
323    * Add a new row at the top
324    */
325   protected addRowTop(): void {
326     const newRow = [];
327     for (let i = 0; i < this.colCount; i++) {
328       const newCell = this.getNewCell();
329       newCell.name = i + 'x' + this.data.length;
330       newRow[i] = newCell;
331     }
332     this.data.unshift(newRow);
333     this.rowCount++;
334   }
335
336   /**
337    * Add a new row at the bottom
338    */
339   protected addRowBottom(): void {
340     const newRow = [];
341     for (let i = 0; i < this.colCount; i++) {
342       const newCell = this.getNewCell();
343       newCell.name = i + 'x' + this.data.length;
344       newRow[i] = newCell;
345     }
346     this.data.push(newRow);
347     this.rowCount++;
348   }
349
350   /**
351    * Removes the first row of the grid and adjusts all cells that might be effected
352    * by that change. (Removing colspans)
353    */
354   protected removeRowTop(): boolean {
355     if (this.rowCount <= 1) {
356       return false;
357     }
358     const newData = [];
359     for (let rowIndex = 1; rowIndex < this.rowCount; rowIndex++) {
360       newData.push(this.data[rowIndex]);
361     }
362
363     // fix rowspan in former last row
364     for (let colIndex = 0; colIndex < this.colCount; colIndex++) {
365       if (this.data[0][colIndex].spanned === 1) {
366         this.findUpperCellWidthRowspanAndDecreaseByOne(colIndex, 0);
367       }
368     }
369
370     this.data = newData;
371     this.rowCount--;
372     return true;
373   }
374
375   /**
376    * Removes the last row of the grid and adjusts all cells that might be effected
377    * by that change. (Removing colspans)
378    */
379   protected removeRowBottom(): boolean {
380     if (this.rowCount <= 1) {
381       return false;
382     }
383     const newData = [];
384     for (let rowIndex = 0; rowIndex < this.rowCount - 1; rowIndex++) {
385       newData.push(this.data[rowIndex]);
386     }
387
388     // fix rowspan in former last row
389     for (let colIndex = 0; colIndex < this.colCount; colIndex++) {
390       if (this.data[this.rowCount - 1][colIndex].spanned === 1) {
391         this.findUpperCellWidthRowspanAndDecreaseByOne(colIndex, this.rowCount - 1);
392       }
393     }
394
395     this.data = newData;
396     this.rowCount--;
397     return true;
398   }
399
400   /**
401    * Takes a cell and looks above it if there are any cells that have colspans that
402    * spans into the given cell. This is used when a row was removed from the grid
403    * to make sure that no cell with wrong colspans exists in the grid.
404    *
405    * @param {number} col
406    * @param {number} row integer
407    */
408   protected findUpperCellWidthRowspanAndDecreaseByOne(col: number, row: number): boolean {
409     const upperCell = this.getCell(col, row - 1);
410     if (!upperCell) {
411       return false;
412     }
413
414     if (upperCell.spanned === 1) {
415       this.findUpperCellWidthRowspanAndDecreaseByOne(col, row - 1);
416     } else {
417       if (upperCell.rowspan > 1) {
418         this.removeRowspan(col, row - 1);
419       }
420     }
421     return true;
422   }
423
424   /**
425    * Removes the outermost right column from the grid.
426    */
427   protected removeColumn(): boolean {
428     if (this.colCount <= 1) {
429       return false;
430     }
431     const newData = [];
432
433     for (let rowIndex = 0; rowIndex < this.rowCount; rowIndex++) {
434       const newRow = [];
435       for (let colIndex = 0; colIndex < this.colCount - 1; colIndex++) {
436         newRow.push(this.data[rowIndex][colIndex]);
437       }
438       if (this.data[rowIndex][this.colCount - 1].spanned === 1) {
439         this.findLeftCellWidthColspanAndDecreaseByOne(this.colCount - 1, rowIndex);
440       }
441       newData.push(newRow);
442     }
443
444     this.data = newData;
445     this.colCount--;
446     return true;
447   }
448
449   /**
450    * Checks if there are any cells on the left side of a given cell with a
451    * rowspan that spans over the given cell.
452    *
453    * @param {number} col
454    * @param {number} row
455    */
456   protected findLeftCellWidthColspanAndDecreaseByOne(col: number, row: number): boolean {
457     const leftCell = this.getCell(col - 1, row);
458     if (!leftCell) {
459       return false;
460     }
461
462     if (leftCell.spanned === 1) {
463       this.findLeftCellWidthColspanAndDecreaseByOne(col - 1, row);
464     } else {
465       if (leftCell.colspan > 1) {
466         this.removeColspan(col - 1, row);
467       }
468     }
469     return true;
470   }
471
472   /**
473    * Adds a column at the right side of the grid.
474    */
475   protected addColumn(): void {
476     for (let rowIndex = 0; rowIndex < this.rowCount; rowIndex++) {
477       const newCell = this.getNewCell();
478       newCell.name = this.colCount + 'x' + rowIndex;
479       this.data[rowIndex].push(newCell);
480     }
481     this.colCount++;
482   }
483
484   /**
485    * Draws the grid as table into a given container.
486    * It also adds all needed links and bindings to the cells to make it editable.
487    */
488   protected drawTable(): void {
489     const $colgroup = $('<colgroup>');
490     for (let col = 0; col < this.colCount; col++) {
491       const percent = 100 / this.colCount;
492       $colgroup.append($('<col>').css({
493         width: parseInt(percent.toString(), 10) + '%',
494       }));
495     }
496     const $table = $('<table id="base" class="table editor">');
497     $table.append($colgroup);
498
499     for (let row = 0; row < this.rowCount; row++) {
500       const rowData = this.data[row];
501       if (rowData.length === 0) {
502         continue;
503       }
504
505       const $row = $('<tr>');
506
507       for (let col = 0; col < this.colCount; col++) {
508         const cell = this.data[row][col];
509         if (cell.spanned === 1) {
510           continue;
511         }
512         const percentRow = 100 / this.rowCount;
513         const percentCol = 100 / this.colCount;
514         const $cell = $('<td>').css({
515           height: parseInt(percentRow.toString(), 10) * cell.rowspan + '%',
516           width: parseInt(percentCol.toString(), 10) * cell.colspan + '%',
517         });
518         const $container = $('<div class="cell_container">');
519         $cell.append($container);
520         const $anchor = $('<a href="#" data-col="' + col + '" data-row="' + row + '">');
521
522         $container.append(
523           $anchor
524             .clone()
525             .attr('class', 't3js-grideditor-link-editor link link_editor')
526             .attr('title', TYPO3.lang.grid_editCell),
527         );
528         if (this.cellCanSpanRight(col, row)) {
529           $container.append(
530             $anchor
531               .clone()
532               .attr('class', 't3js-grideditor-link-expand-right link link_expand_right')
533               .attr('title', TYPO3.lang.grid_mergeCell),
534           );
535         }
536         if (this.cellCanShrinkLeft(col, row)) {
537           $container.append(
538             $anchor
539               .clone()
540               .attr('class', 't3js-grideditor-link-shrink-left link link_shrink_left')
541               .attr('title', TYPO3.lang.grid_splitCell),
542           );
543         }
544         if (this.cellCanSpanDown(col, row)) {
545           $container.append(
546             $anchor
547               .clone()
548               .attr('class', 't3js-grideditor-link-expand-down link link_expand_down')
549               .attr('title', TYPO3.lang.grid_mergeCell),
550           );
551         }
552         if (this.cellCanShrinkUp(col, row)) {
553           $container.append(
554             $anchor
555               .clone()
556               .attr('class', 't3js-grideditor-link-shrink-up link link_shrink_up')
557               .attr('title', TYPO3.lang.grid_splitCell),
558           );
559         }
560         $cell.append(
561           $('<div class="cell_data">')
562             .html(
563               TYPO3.lang.grid_name + ': '
564               + (cell.name ? GridEditor.stripMarkup(cell.name) : TYPO3.lang.grid_notSet)
565               + '<br />'
566               + TYPO3.lang.grid_column + ': '
567               + (typeof cell.column === 'undefined' || isNaN(cell.column)
568                   ? TYPO3.lang.grid_notSet
569                   : parseInt(cell.column, 10)
570                 ),
571             ),
572         );
573         if (cell.colspan > 1) {
574           $cell.attr('colspan', cell.colspan);
575         }
576         if (cell.rowspan > 1) {
577           $cell.attr('rowspan', cell.rowspan);
578         }
579         $row.append($cell);
580       }
581       $table.append($row);
582     }
583     $(this.targetElement).empty().append($table);
584   }
585
586   /**
587    * Sets the name of a certain grid element.
588    *
589    * @param {String} newName
590    * @param {number} col
591    * @param {number} row
592    *
593    * @returns {Boolean}
594    */
595   protected setName(newName: string, col: number, row: number): boolean {
596     const cell = this.getCell(col, row);
597     if (!cell) {
598       return false;
599     }
600     cell.name = GridEditor.stripMarkup(newName);
601     return true;
602   }
603
604   /**
605    * Sets the column field for a certain grid element. This is NOT the column of the
606    * element itself.
607    *
608    * @param {number} newColumn
609    * @param {number} col
610    * @param {number} row
611    *
612    * @returns {Boolean}
613    */
614   protected setColumn(newColumn: number, col: number, row: number): boolean {
615     const cell = this.getCell(col, row);
616     if (!cell) {
617       return false;
618     }
619     cell.column = parseInt(newColumn.toString(), 10);
620     return true;
621   }
622
623   /**
624    * Creates an ExtJs Window with two input fields and shows it. On save, the data
625    * is written into the grid element.
626    *
627    * @param {number} col
628    * @param {number} row
629    *
630    * @returns {Boolean}
631    */
632   protected showOptions(col: number, row: number): boolean {
633     const cell = this.getCell(col, row);
634     if (!cell) {
635       return false;
636     }
637     let colPos;
638     if (cell.column === 0) {
639       colPos = 0;
640     } else if (cell.column) {
641       colPos = parseInt(cell.column.toString(), 10);
642     } else {
643       colPos = '';
644     }
645
646     const $markup = $('<div>');
647     const $formGroup = $('<div class="form-group">');
648     const $label = $('<label>');
649     const $input = $('<input>');
650
651     $markup.append([
652       $formGroup
653         .clone()
654         .append([
655           $label
656             .clone()
657             .text(TYPO3.lang.grid_nameHelp)
658           ,
659           $input
660             .clone()
661             .attr('type', 'text')
662             .attr('class', 't3js-grideditor-field-name form-control')
663             .attr('name', 'name')
664             .val(GridEditor.stripMarkup(cell.name) || ''),
665         ]),
666       $formGroup
667         .clone()
668         .append([
669           $label
670             .clone()
671             .text(TYPO3.lang.grid_columnHelp)
672           ,
673           $input
674             .clone()
675             .attr('type', 'text')
676             .attr('class', 't3js-grideditor-field-colpos form-control')
677             .attr('name', 'column')
678             .val(colPos),
679         ]),
680     ]);
681
682     const $modal = Modal.show(TYPO3.lang.grid_windowTitle, $markup, SeverityEnum.notice, [
683       {
684         active: true,
685         btnClass: 'btn-default',
686         name: 'cancel',
687         text: $(this).data('button-close-text') || TYPO3.lang['button.cancel'] || 'Cancel',
688       },
689       {
690         btnClass: 'btn-primary',
691         name: 'ok',
692         text: $(this).data('button-ok-text') || TYPO3.lang['button.ok'] || 'OK',
693       },
694     ]);
695     $modal.data('col', col);
696     $modal.data('row', row);
697     $modal.on('button.clicked', this.modalButtonClickHandler);
698     return true;
699   }
700
701   /**
702    * Returns a cell element from the grid.
703    *
704    * @param {number} col
705    * @param {number} row
706    */
707   protected getCell(col: number, row: number): any {
708     if (col > this.colCount - 1) {
709       return false;
710     }
711     if (row > this.rowCount - 1) {
712       return false;
713     }
714     if (this.data.length > row - 1 && this.data[row].length > col - 1) {
715       return this.data[row][col];
716     }
717     return null;
718   }
719
720   /**
721    * Checks whether a cell can span to the right or not. A cell can span to the right
722    * if it is not in the last column and if there is no cell beside it that is
723    * already overspanned by some other cell.
724    *
725    * @param {number} col
726    * @param {number} row
727    * @returns {Boolean}
728    */
729   protected cellCanSpanRight(col: number, row: number): boolean {
730     if (col === this.colCount - 1) {
731       return false;
732     }
733
734     const cell = this.getCell(col, row);
735     let checkCell;
736     if (cell.rowspan > 1) {
737       for (let rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) {
738         checkCell = this.getCell(col + cell.colspan, rowIndex);
739         if (!checkCell || checkCell.spanned === 1 || checkCell.colspan > 1 || checkCell.rowspan > 1) {
740           return false;
741         }
742       }
743     } else {
744       checkCell = this.getCell(col + cell.colspan, row);
745       if (!checkCell || cell.spanned === 1 || checkCell.spanned === 1 || checkCell.colspan > 1
746         || checkCell.rowspan > 1) {
747         return false;
748       }
749     }
750
751     return true;
752   }
753
754   /**
755    * Checks whether a cell can span down or not.
756    *
757    * @param {number} col
758    * @param {number} row
759    * @returns {Boolean}
760    */
761   protected cellCanSpanDown(col: number, row: number): boolean {
762     if (row === this.rowCount - 1) {
763       return false;
764     }
765
766     const cell = this.getCell(col, row);
767     let checkCell;
768     if (cell.colspan > 1) {
769       // we have to check all cells on the right side for the complete colspan
770       for (let colIndex = col; colIndex < col + cell.colspan; colIndex++) {
771         checkCell = this.getCell(colIndex, row + cell.rowspan);
772         if (!checkCell || checkCell.spanned === 1 || checkCell.colspan > 1 || checkCell.rowspan > 1) {
773           return false;
774         }
775       }
776     } else {
777       checkCell = this.getCell(col, row + cell.rowspan);
778       if (!checkCell || cell.spanned === 1 || checkCell.spanned === 1 || checkCell.colspan > 1
779         || checkCell.rowspan > 1) {
780         return false;
781       }
782     }
783
784     return true;
785   }
786
787   /**
788    * Checks if a cell can shrink to the left. It can shrink if the colspan of the
789    * cell is bigger than 1.
790    *
791    * @param {number} col
792    * @param {number} row
793    * @returns {Boolean}
794    */
795   protected cellCanShrinkLeft(col: number, row: number): boolean {
796     return (this.data[row][col].colspan > 1);
797   }
798
799   /**
800    * Returns if a cell can shrink up. This is the case if a cell has at least
801    * a rowspan of 2.
802    *
803    * @param {number} col
804    * @param {number} row
805    * @returns {Boolean}
806    */
807   protected cellCanShrinkUp(col: number, row: number): boolean {
808     return (this.data[row][col].rowspan > 1);
809   }
810
811   /**
812    * Adds a colspan to a grid element.
813    *
814    * @param {number} col
815    * @param {number} row
816    * @returns {Boolean}
817    */
818   protected addColspan(col: number, row: number): boolean {
819     const cell = this.getCell(col, row);
820     if (!cell || !this.cellCanSpanRight(col, row)) {
821       return false;
822     }
823
824     for (let rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) {
825       this.data[rowIndex][col + cell.colspan].spanned = 1;
826     }
827     cell.colspan += 1;
828     return true;
829   }
830
831   /**
832    * Adds a rowspan to grid element.
833    *
834    * @param {number} col
835    * @param {number} row
836    * @returns {Boolean}
837    */
838   protected addRowspan(col: number, row: number): boolean {
839     const cell = this.getCell(col, row);
840     if (!cell || !this.cellCanSpanDown(col, row)) {
841       return false;
842     }
843
844     for (let colIndex = col; colIndex < col + cell.colspan; colIndex++) {
845       this.data[row + cell.rowspan][colIndex].spanned = 1;
846     }
847     cell.rowspan += 1;
848     return true;
849   }
850
851   /**
852    * Removes a colspan from a grid element.
853    *
854    * @param {number} col
855    * @param {number} row
856    * @returns {Boolean}
857    */
858   protected removeColspan(col: number, row: number): boolean {
859     const cell = this.getCell(col, row);
860     if (!cell || !this.cellCanShrinkLeft(col, row)) {
861       return false;
862     }
863
864     cell.colspan -= 1;
865
866     for (let rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) {
867       this.data[rowIndex][col + cell.colspan].spanned = 0;
868     }
869     return true;
870   }
871
872   /**
873    * Removes a rowspan from a grid element.
874    *
875    * @param {number} col
876    * @param {number} row
877    * @returns {Boolean}
878    */
879   protected removeRowspan(col: number, row: number): boolean {
880     const cell = this.getCell(col, row);
881     if (!cell || !this.cellCanShrinkUp(col, row)) {
882       return false;
883     }
884
885     cell.rowspan -= 1;
886     for (let colIndex = col; colIndex < col + cell.colspan; colIndex++) {
887       this.data[row + cell.rowspan][colIndex].spanned = 0;
888     }
889     return true;
890   }
891
892   /**
893    * Exports the current grid to a TypoScript notation that can be read by the
894    * page module and is human readable.
895    *
896    * @returns {String}
897    */
898   protected export2LayoutRecord(): string {
899     let result = 'backend_layout {\n\tcolCount = ' + this.colCount + '\n\trowCount = ' + this.rowCount + '\n\trows {\n';
900     for (let row = 0; row < this.rowCount; row++) {
901       result += '\t\t' + (row + 1) + ' {\n';
902       result += '\t\t\tcolumns {\n';
903       let colIndex = 0;
904       for (let col = 0; col < this.colCount; col++) {
905         const cell = this.getCell(col, row);
906         if (cell) {
907           if (!cell.spanned) {
908             colIndex++;
909             result += '\t\t\t\t' + (colIndex) + ' {\n';
910             result += '\t\t\t\t\tname = ' + ((!cell.name) ? col + 'x' + row : cell.name) + '\n';
911             if (cell.colspan > 1) {
912               result += '\t\t\t\t\tcolspan = ' + cell.colspan + '\n';
913             }
914             if (cell.rowspan > 1) {
915               result += '\t\t\t\t\trowspan = ' + cell.rowspan + '\n';
916             }
917             if (typeof(cell.column) === 'number') {
918               result += '\t\t\t\t\tcolPos = ' + cell.column + '\n';
919             }
920             result += '\t\t\t\t}\n';
921           }
922         }
923
924       }
925       result += '\t\t\t}\n';
926       result += '\t\t}\n';
927     }
928
929     result += '\t}\n}\n';
930     return result;
931   }
932 }