[TASK] Migrate Severity to TypeScript
[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 'bootstrap';
15 import * as $ from 'jquery';
16 import Modal = require('TYPO3/CMS/Backend/Modal');
17 import Severity = require('./Severity');
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, Severity.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 }