2 * This file is part of the TYPO3 CMS project.
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.
8 * For the full copyright and license information, please read the
9 * LICENSE.txt file that was distributed with this source code.
11 * The TYPO3 project - inspiring people to share!
15 import * as $ from 'jquery';
16 import Modal = require('TYPO3/CMS/Backend/Modal');
17 import Severity = require('TYPO3/CMS/Backend/Severity');
20 * GridEditorConfigurationInterface
22 interface GridEditorConfigurationInterface {
30 interface CellInterface {
40 * Module: TYPO3/CMS/Backend/GridEditor
41 * @exports TYPO3/CMS/Backend/GridEditor
43 export class GridEditor {
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';
73 * @param {String} input
76 public static stripMarkup(input: string): string {
77 input = input.replace(/<(.*)>/gi, '');
78 return $('<p>' + input + '</p>').text();
83 * @param {GridEditorConfigurationInterface} config
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();
96 $(this.selectorConfigPreviewButton).empty().append(TYPO3.lang['button.showPageTsConfig']);
98 this.initializeEvents();
100 this.writeConfig(this.export2LayoutRecord());
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);
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') {
131 Modal.currentModal.find('.t3js-grideditor-field-name').val(),
132 Modal.currentModal.data('col'),
133 Modal.currentModal.data('row'),
136 Modal.currentModal.find('.t3js-grideditor-field-colpos').val(),
137 Modal.currentModal.data('col'),
138 Modal.currentModal.data('row'),
141 this.writeConfig(this.export2LayoutRecord());
142 Modal.currentModal.trigger('modal-dismiss');
150 protected addColumnHandler = (e: Event) => {
154 this.writeConfig(this.export2LayoutRecord());
161 protected removeColumnHandler = (e: Event) => {
165 this.writeConfig(this.export2LayoutRecord());
172 protected addRowTopHandler = (e: Event) => {
176 this.writeConfig(this.export2LayoutRecord());
183 protected addRowBottomHandler = (e: Event) => {
187 this.writeConfig(this.export2LayoutRecord());
194 protected removeRowTopHandler = (e: Event) => {
198 this.writeConfig(this.export2LayoutRecord());
205 protected removeRowBottomHandler = (e: Event) => {
207 this.removeRowBottom();
209 this.writeConfig(this.export2LayoutRecord());
216 protected linkEditorHandler = (e: Event) => {
218 const $element = $(e.target);
219 this.showOptions($element.data('col'), $element.data('row'));
226 protected linkExpandRightHandler = (e: Event) => {
228 const $element = $(e.target);
229 this.addColspan($element.data('col'), $element.data('row'));
231 this.writeConfig(this.export2LayoutRecord());
238 protected linkShrinkLeftHandler = (e: Event) => {
240 const $element = $(e.target);
241 this.removeColspan($element.data('col'), $element.data('row'));
243 this.writeConfig(this.export2LayoutRecord());
250 protected linkExpandDownHandler = (e: Event) => {
252 const $element = $(e.target);
253 this.addRowspan($element.data('col'), $element.data('row'));
255 this.writeConfig(this.export2LayoutRecord());
262 protected linkShrinkUpHandler = (e: Event) => {
264 const $element = $(e.target);
265 this.removeRowspan($element.data('col'), $element.data('row'));
267 this.writeConfig(this.export2LayoutRecord());
274 protected configPreviewButtonHandler = (e: Event) => {
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();
282 $button.empty().append(TYPO3.lang['button.hidePageTsConfig']);
283 $(this.selectorConfigPreview).slideDown();
288 * Create a new cell from defaultCell
291 protected getNewCell(): any {
292 return $.extend({}, this.defaultCell);
296 * write data back to hidden field
300 protected writeConfig(data: any): void {
301 this.field.val(data);
302 const configLines = data.split('\n');
304 for (const line of configLines) {
306 config += '\t\t\t' + line + '\n';
309 $(this.selectorConfigPreview).find('code').empty().append(
310 'mod.web_layout.BackendLayouts {\n' +
312 ' title = Example\n' +
313 ' icon = EXT:example_extension/Resources/Public/Images/BackendLayouts/default.gif\n' +
315 config.replace(new RegExp('\t', 'g'), ' ') +
323 * Add a new row at the top
325 protected addRowTop(): void {
327 for (let i = 0; i < this.colCount; i++) {
328 const newCell = this.getNewCell();
329 newCell.name = i + 'x' + this.data.length;
332 this.data.unshift(newRow);
337 * Add a new row at the bottom
339 protected addRowBottom(): void {
341 for (let i = 0; i < this.colCount; i++) {
342 const newCell = this.getNewCell();
343 newCell.name = i + 'x' + this.data.length;
346 this.data.push(newRow);
351 * Removes the first row of the grid and adjusts all cells that might be effected
352 * by that change. (Removing colspans)
354 protected removeRowTop(): boolean {
355 if (this.rowCount <= 1) {
359 for (let rowIndex = 1; rowIndex < this.rowCount; rowIndex++) {
360 newData.push(this.data[rowIndex]);
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);
376 * Removes the last row of the grid and adjusts all cells that might be effected
377 * by that change. (Removing colspans)
379 protected removeRowBottom(): boolean {
380 if (this.rowCount <= 1) {
384 for (let rowIndex = 0; rowIndex < this.rowCount - 1; rowIndex++) {
385 newData.push(this.data[rowIndex]);
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);
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.
405 * @param {number} col
406 * @param {number} row integer
408 protected findUpperCellWidthRowspanAndDecreaseByOne(col: number, row: number): boolean {
409 const upperCell = this.getCell(col, row - 1);
414 if (upperCell.spanned === 1) {
415 this.findUpperCellWidthRowspanAndDecreaseByOne(col, row - 1);
417 if (upperCell.rowspan > 1) {
418 this.removeRowspan(col, row - 1);
425 * Removes the outermost right column from the grid.
427 protected removeColumn(): boolean {
428 if (this.colCount <= 1) {
433 for (let rowIndex = 0; rowIndex < this.rowCount; rowIndex++) {
435 for (let colIndex = 0; colIndex < this.colCount - 1; colIndex++) {
436 newRow.push(this.data[rowIndex][colIndex]);
438 if (this.data[rowIndex][this.colCount - 1].spanned === 1) {
439 this.findLeftCellWidthColspanAndDecreaseByOne(this.colCount - 1, rowIndex);
441 newData.push(newRow);
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.
453 * @param {number} col
454 * @param {number} row
456 protected findLeftCellWidthColspanAndDecreaseByOne(col: number, row: number): boolean {
457 const leftCell = this.getCell(col - 1, row);
462 if (leftCell.spanned === 1) {
463 this.findLeftCellWidthColspanAndDecreaseByOne(col - 1, row);
465 if (leftCell.colspan > 1) {
466 this.removeColspan(col - 1, row);
473 * Adds a column at the right side of the grid.
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);
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.
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) + '%',
496 const $table = $('<table id="base" class="table editor">');
497 $table.append($colgroup);
499 for (let row = 0; row < this.rowCount; row++) {
500 const rowData = this.data[row];
501 if (rowData.length === 0) {
505 const $row = $('<tr>');
507 for (let col = 0; col < this.colCount; col++) {
508 const cell = this.data[row][col];
509 if (cell.spanned === 1) {
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 + '%',
518 const $container = $('<div class="cell_container">');
519 $cell.append($container);
520 const $anchor = $('<a href="#" data-col="' + col + '" data-row="' + row + '">');
525 .attr('class', 't3js-grideditor-link-editor link link_editor')
526 .attr('title', TYPO3.lang.grid_editCell),
528 if (this.cellCanSpanRight(col, row)) {
532 .attr('class', 't3js-grideditor-link-expand-right link link_expand_right')
533 .attr('title', TYPO3.lang.grid_mergeCell),
536 if (this.cellCanShrinkLeft(col, row)) {
540 .attr('class', 't3js-grideditor-link-shrink-left link link_shrink_left')
541 .attr('title', TYPO3.lang.grid_splitCell),
544 if (this.cellCanSpanDown(col, row)) {
548 .attr('class', 't3js-grideditor-link-expand-down link link_expand_down')
549 .attr('title', TYPO3.lang.grid_mergeCell),
552 if (this.cellCanShrinkUp(col, row)) {
556 .attr('class', 't3js-grideditor-link-shrink-up link link_shrink_up')
557 .attr('title', TYPO3.lang.grid_splitCell),
561 $('<div class="cell_data">')
563 TYPO3.lang.grid_name + ': '
564 + (cell.name ? GridEditor.stripMarkup(cell.name) : TYPO3.lang.grid_notSet)
566 + TYPO3.lang.grid_column + ': '
567 + (typeof cell.column === 'undefined' || isNaN(cell.column)
568 ? TYPO3.lang.grid_notSet
569 : parseInt(cell.column, 10)
573 if (cell.colspan > 1) {
574 $cell.attr('colspan', cell.colspan);
576 if (cell.rowspan > 1) {
577 $cell.attr('rowspan', cell.rowspan);
583 $(this.targetElement).empty().append($table);
587 * Sets the name of a certain grid element.
589 * @param {String} newName
590 * @param {number} col
591 * @param {number} row
595 protected setName(newName: string, col: number, row: number): boolean {
596 const cell = this.getCell(col, row);
600 cell.name = GridEditor.stripMarkup(newName);
605 * Sets the column field for a certain grid element. This is NOT the column of the
608 * @param {number} newColumn
609 * @param {number} col
610 * @param {number} row
614 protected setColumn(newColumn: number, col: number, row: number): boolean {
615 const cell = this.getCell(col, row);
619 cell.column = parseInt(newColumn.toString(), 10);
624 * Creates an ExtJs Window with two input fields and shows it. On save, the data
625 * is written into the grid element.
627 * @param {number} col
628 * @param {number} row
632 protected showOptions(col: number, row: number): boolean {
633 const cell = this.getCell(col, row);
638 if (cell.column === 0) {
640 } else if (cell.column) {
641 colPos = parseInt(cell.column.toString(), 10);
646 const $markup = $('<div>');
647 const $formGroup = $('<div class="form-group">');
648 const $label = $('<label>');
649 const $input = $('<input>');
657 .text(TYPO3.lang.grid_nameHelp)
661 .attr('type', 'text')
662 .attr('class', 't3js-grideditor-field-name form-control')
663 .attr('name', 'name')
664 .val(GridEditor.stripMarkup(cell.name) || ''),
671 .text(TYPO3.lang.grid_columnHelp)
675 .attr('type', 'text')
676 .attr('class', 't3js-grideditor-field-colpos form-control')
677 .attr('name', 'column')
682 const $modal = Modal.show(TYPO3.lang.grid_windowTitle, $markup, Severity.notice, [
685 btnClass: 'btn-default',
687 text: $(this).data('button-close-text') || TYPO3.lang['button.cancel'] || 'Cancel',
690 btnClass: 'btn-primary',
692 text: $(this).data('button-ok-text') || TYPO3.lang['button.ok'] || 'OK',
695 $modal.data('col', col);
696 $modal.data('row', row);
697 $modal.on('button.clicked', this.modalButtonClickHandler);
702 * Returns a cell element from the grid.
704 * @param {number} col
705 * @param {number} row
707 protected getCell(col: number, row: number): any {
708 if (col > this.colCount - 1) {
711 if (row > this.rowCount - 1) {
714 if (this.data.length > row - 1 && this.data[row].length > col - 1) {
715 return this.data[row][col];
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.
725 * @param {number} col
726 * @param {number} row
729 protected cellCanSpanRight(col: number, row: number): boolean {
730 if (col === this.colCount - 1) {
734 const cell = this.getCell(col, row);
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) {
744 checkCell = this.getCell(col + cell.colspan, row);
745 if (!checkCell || cell.spanned === 1 || checkCell.spanned === 1 || checkCell.colspan > 1
746 || checkCell.rowspan > 1) {
755 * Checks whether a cell can span down or not.
757 * @param {number} col
758 * @param {number} row
761 protected cellCanSpanDown(col: number, row: number): boolean {
762 if (row === this.rowCount - 1) {
766 const cell = this.getCell(col, row);
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) {
777 checkCell = this.getCell(col, row + cell.rowspan);
778 if (!checkCell || cell.spanned === 1 || checkCell.spanned === 1 || checkCell.colspan > 1
779 || checkCell.rowspan > 1) {
788 * Checks if a cell can shrink to the left. It can shrink if the colspan of the
789 * cell is bigger than 1.
791 * @param {number} col
792 * @param {number} row
795 protected cellCanShrinkLeft(col: number, row: number): boolean {
796 return (this.data[row][col].colspan > 1);
800 * Returns if a cell can shrink up. This is the case if a cell has at least
803 * @param {number} col
804 * @param {number} row
807 protected cellCanShrinkUp(col: number, row: number): boolean {
808 return (this.data[row][col].rowspan > 1);
812 * Adds a colspan to a grid element.
814 * @param {number} col
815 * @param {number} row
818 protected addColspan(col: number, row: number): boolean {
819 const cell = this.getCell(col, row);
820 if (!cell || !this.cellCanSpanRight(col, row)) {
824 for (let rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) {
825 this.data[rowIndex][col + cell.colspan].spanned = 1;
832 * Adds a rowspan to grid element.
834 * @param {number} col
835 * @param {number} row
838 protected addRowspan(col: number, row: number): boolean {
839 const cell = this.getCell(col, row);
840 if (!cell || !this.cellCanSpanDown(col, row)) {
844 for (let colIndex = col; colIndex < col + cell.colspan; colIndex++) {
845 this.data[row + cell.rowspan][colIndex].spanned = 1;
852 * Removes a colspan from a grid element.
854 * @param {number} col
855 * @param {number} row
858 protected removeColspan(col: number, row: number): boolean {
859 const cell = this.getCell(col, row);
860 if (!cell || !this.cellCanShrinkLeft(col, row)) {
866 for (let rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) {
867 this.data[rowIndex][col + cell.colspan].spanned = 0;
873 * Removes a rowspan from a grid element.
875 * @param {number} col
876 * @param {number} row
879 protected removeRowspan(col: number, row: number): boolean {
880 const cell = this.getCell(col, row);
881 if (!cell || !this.cellCanShrinkUp(col, row)) {
886 for (let colIndex = col; colIndex < col + cell.colspan; colIndex++) {
887 this.data[row + cell.rowspan][colIndex].spanned = 0;
893 * Exports the current grid to a TypoScript notation that can be read by the
894 * page module and is human readable.
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';
904 for (let col = 0; col < this.colCount; col++) {
905 const cell = this.getCell(col, row);
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';
914 if (cell.rowspan > 1) {
915 result += '\t\t\t\t\trowspan = ' + cell.rowspan + '\n';
917 if (typeof(cell.column) === 'number') {
918 result += '\t\t\t\t\tcolPos = ' + cell.column + '\n';
920 result += '\t\t\t\t}\n';
925 result += '\t\t\t}\n';
929 result += '\t}\n}\n';