8c942376167a77a561fa614f67328b17cefb6e36
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Resources / Public / JavaScript / GridEditor.js
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 /**
15 * Module: TYPO3/CMS/Backend/GridEditor
16 */
17 define(['jquery', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/Severity', 'bootstrap'], function($, Modal, Severity) {
18 'use strict';
19
20 /**
21 * The main ContextHelp object
22 *
23 * @type {{colCount: number, rowCount: number, data: {}, nameLabel: string, columnLabel: string, targetElement: null}}
24 * @exports TYPO3/CMS/Backend/GridEditor
25 */
26 var GridEditor = {
27 selectorEditor: '.t3js-grideditor',
28 selectorAddColumn: '.t3js-grideditor-addcolumn',
29 selectorRemoveColumn: '.t3js-grideditor-removecolumn',
30 selectorAddRow: '.t3js-grideditor-addrow',
31 selectorRemoveRow: '.t3js-grideditor-removerow',
32 selectorLinkEditor: '.t3js-grideditor-link-editor',
33 selectorLinkExpandRight: '.t3js-grideditor-link-expand-right',
34 selectorLinkShrinkLeft: '.t3js-grideditor-link-shrink-left',
35 selectorLinkExpandDown: '.t3js-grideditor-link-expand-down',
36 selectorLinkShrinkUp: '.t3js-grideditor-link-shrink-up',
37 selectorDocHeaderSave: '.t3js-grideditor-savedok',
38 selectorDocHeaderSaveClose: '.t3js-grideditor-savedokclose',
39 colCount: 1,
40 rowCount: 1,
41 data: [],
42 nameLabel: 'name',
43 columnLabel: 'columen label',
44 targetElement: null
45 };
46
47 /**
48 *
49 * @param {Object} config
50 */
51 GridEditor.initialize = function(config) {
52 config = config || {};
53 var $element = $(GridEditor.selectorEditor);
54 GridEditor.colCount = $element.data('colcount');
55 GridEditor.rowCount = $element.data('rowcount');
56 GridEditor.data = $element.data('data');
57 GridEditor.nameLabel = config.nameLabel || 'Name';
58 GridEditor.columnLabel = config.columnLabel || 'Column';
59 GridEditor.targetElement = $(GridEditor.selectorEditor);
60
61 $(document).on('click', GridEditor.selectorDocHeaderSave, function(e) {
62 e.preventDefault();
63 storeData(GridEditor.export2LayoutRecord());
64 });
65 $(document).on('click', GridEditor.selectorDocHeaderSaveClose, function(e) {
66 e.preventDefault();
67 storeData(GridEditor.export2LayoutRecord());
68 window.close();
69 });
70 $(document).on('click', GridEditor.selectorAddColumn, function(e) {
71 e.preventDefault();
72 GridEditor.addColumn();
73 GridEditor.drawTable();
74 });
75 $(document).on('click', GridEditor.selectorRemoveColumn, function(e) {
76 e.preventDefault();
77 GridEditor.removeColumn();
78 GridEditor.drawTable();
79 });
80 $(document).on('click', GridEditor.selectorAddRow, function(e) {
81 e.preventDefault();
82 GridEditor.addRow();
83 GridEditor.drawTable();
84 });
85 $(document).on('click', GridEditor.selectorRemoveRow, function(e) {
86 e.preventDefault();
87 GridEditor.removeRow();
88 GridEditor.drawTable();
89 });
90 $(document).on('click', GridEditor.selectorLinkEditor, function(e) {
91 e.preventDefault();
92 var $element = $(this);
93 var col = $element.data('col');
94 var row = $element.data('row');
95 GridEditor.showOptions(col, row);
96 });
97 $(document).on('click', GridEditor.selectorLinkExpandRight, function(e) {
98 e.preventDefault();
99 var $element = $(this);
100 var col = $element.data('col');
101 var row = $element.data('row');
102 GridEditor.addColspan(col, row);
103 GridEditor.drawTable();
104 });
105 $(document).on('click', GridEditor.selectorLinkShrinkLeft, function(e) {
106 e.preventDefault();
107 var $element = $(this);
108 var col = $element.data('col');
109 var row = $element.data('row');
110 GridEditor.removeColspan(col, row);
111 GridEditor.drawTable();
112 });
113 $(document).on('click', GridEditor.selectorLinkExpandDown, function(e) {
114 e.preventDefault();
115 var $element = $(this);
116 var col = $element.data('col');
117 var row = $element.data('row');
118 GridEditor.addRowspan(col, row);
119 GridEditor.drawTable();
120 });
121 $(document).on('click', GridEditor.selectorLinkShrinkUp, function(e) {
122 e.preventDefault();
123 var $element = $(this);
124 var col = $element.data('col');
125 var row = $element.data('row');
126 GridEditor.removeRowspan(col, row);
127 GridEditor.drawTable();
128 });
129
130 GridEditor.drawTable();
131 };
132
133 /**
134 * Add a new row
135 */
136 GridEditor.addRow = function() {
137 var newRow = [];
138 for (var i = 0; i < GridEditor.colCount; i++) {
139 newRow[i] = {spanned: false, rowspan: 1, colspan: 1};
140 }
141 GridEditor.data.push(newRow);
142 GridEditor.rowCount++;
143 };
144
145 /**
146 * Removes the last row of the grid and adjusts all cells that might be effected
147 * by that change. (Removing colspans)
148 */
149 GridEditor.removeRow = function() {
150 if (GridEditor.rowCount <= 1) {
151 return false;
152 }
153 var newData = [];
154 for (var rowIndex = 0; rowIndex < GridEditor.rowCount - 1; rowIndex++) {
155 newData.push(GridEditor.data[rowIndex]);
156 }
157
158 // fix rowspan in former last row
159 for (var colIndex = 0; colIndex < GridEditor.colCount; colIndex++) {
160 if (GridEditor.data[GridEditor.rowCount - 1][colIndex].spanned == true) {
161 GridEditor.findUpperCellWidthRowspanAndDecreaseByOne(colIndex, GridEditor.rowCount - 1);
162 }
163 }
164
165 GridEditor.data = newData;
166 GridEditor.rowCount--;
167 };
168
169 /**
170 * Takes a cell and looks above it if there are any cells that have colspans that
171 * spans into the given cell. This is used when a row was removed from the grid
172 * to make sure that no cell with wrong colspans exists in the grid.
173 *
174 * @param {Integer} col
175 * @param {Integer} row integer
176 */
177 GridEditor.findUpperCellWidthRowspanAndDecreaseByOne = function(col, row) {
178 var upperCell = GridEditor.getCell(col, row - 1);
179 if (!upperCell) {
180 return false;
181 }
182
183 if (upperCell.spanned == true) {
184 GridEditor.findUpperCellWidthRowspanAndDecreaseByOne(col, row - 1);
185 } else {
186 if (upperCell.rowspan > 1) {
187 GridEditor.removeRowspan(col, row - 1);
188 }
189 }
190 };
191
192 /**
193 * Removes the outermost right column from the grid.
194 */
195 GridEditor.removeColumn = function() {
196 if (GridEditor.colCount <= 1) {
197 return false;
198 }
199 var newData = [];
200
201 for (var rowIndex = 0; rowIndex < GridEditor.rowCount; rowIndex++) {
202 var newRow = [];
203 for (var colIndex = 0; colIndex < GridEditor.colCount - 1; colIndex++) {
204 newRow.push(GridEditor.data[rowIndex][colIndex]);
205 }
206 if (GridEditor.data[rowIndex][GridEditor.colCount - 1].spanned == true) {
207 GridEditor.findLeftCellWidthColspanAndDecreaseByOne(GridEditor.colCount - 1, rowIndex);
208 }
209 newData.push(newRow);
210 }
211
212 GridEditor.data = newData;
213 GridEditor.colCount--;
214 };
215
216 /**
217 * Checks if there are any cells on the left side of a given cell with a
218 * rowspan that spans over the given cell.
219 *
220 * @param {Integer} col
221 * @param {Integer} row
222 */
223 GridEditor.findLeftCellWidthColspanAndDecreaseByOne = function(col, row) {
224 var leftCell = GridEditor.getCell(col - 1, row);
225 if (!leftCell) {
226 return false;
227 }
228
229 if (leftCell.spanned == true) {
230 GridEditor.findLeftCellWidthColspanAndDecreaseByOne(col - 1, row);
231 } else {
232 if (leftCell.colspan > 1) {
233 GridEditor.removeColspan(col - 1, row);
234 }
235 }
236 };
237
238 /**
239 * Adds a column at the right side of the grid.
240 */
241 GridEditor.addColumn = function() {
242 for (var rowIndex = 0; rowIndex < GridEditor.rowCount; rowIndex++) {
243 GridEditor.data[rowIndex].push({
244 spanned: false,
245 rowspan: 1,
246 colspan: 1,
247 name: GridEditor.colCount + 'x' + rowIndex
248 });
249 }
250 GridEditor.colCount++;
251 };
252
253 /**
254 * Draws the grid as table into a given container.
255 * It also adds all needed links and bindings to the cells to make it editable.
256 */
257 GridEditor.drawTable = function() {
258 var col;
259 var $colgroup = $('<colgroup>');
260 for (col = 0; col < GridEditor.colCount; col++) {
261 $colgroup.append($('<col>').css({
262 width: parseInt(100 / GridEditor.colCount, 10) + '%'
263 }));
264 }
265 var $table = $('<table id="base" class="table editor">');
266 $table.append($colgroup);
267
268 for (var row = 0; row < GridEditor.rowCount; row++) {
269 var rowData = GridEditor.data[row];
270 if (rowData.length == 0) {
271 continue;
272 }
273
274 var $row = $('<tr>');
275
276 for (col = 0; col < GridEditor.colCount; col++) {
277 var cell = GridEditor.data[row][col];
278 if (cell.spanned == true) {
279 continue;
280 }
281 var $cell = $('<td>').css({
282 height: parseInt(100 / GridEditor.rowCount, 10) * cell.rowspan + '%',
283 width: parseInt(100 / GridEditor.colCount, 10) * cell.colspan + '%'
284 });
285 var $container = $('<div class="cell_container">');
286 $cell.append($container);
287 var dataString = ' data-col="' + col + '" data-row="' + row + '"';
288 $container.append($('<a class="t3js-grideditor-link-editor link link_editor" title="' + TYPO3.lang['editCell'] + '" ' + dataString + ' href="#"><!-- --></a>'));
289 if (GridEditor.cellCanSpanRight(col, row)) {
290 $container.append($('<a class="t3js-grideditor-link-expand-right link link_expand_right" href="#" title="' + TYPO3.lang['mergeCell'] + '" ' + dataString + '><!-- --></a>'));
291 }
292 if (GridEditor.cellCanShrinkLeft(col, row)) {
293 $container.append('<a class="t3js-grideditor-link-shrink-left link link_shrink_left" href="#" title="' + TYPO3.lang['splitCell'] + '" ' + dataString + '><!-- --></a>');
294 }
295 if (GridEditor.cellCanSpanDown(col, row)) {
296 $container.append('<a class="t3js-grideditor-link-expand-down link link_expand_down" href="#" title="' + TYPO3.lang['mergeCell'] + '" ' + dataString + '><!-- --></a>');
297 }
298 if (GridEditor.cellCanShrinkUp(col, row)) {
299 $container.append('<a class="t3js-grideditor-link-shrink-up link link_shrink_up" href="#" title="' + TYPO3.lang['splitCell'] + '" ' + dataString + '><!-- --></a>');
300 }
301 $cell.append('<div class="cell_data">' + TYPO3.lang['name'] + ': ' + (cell.name ? GridEditor.stripMarkup(cell.name) : TYPO3.lang['notSet'])
302 + '<br />' + TYPO3.lang['column'] + ': '
303 + (cell.column === undefined ? TYPO3.lang['notSet'] : parseInt(cell.column, 10)) + '</div>');
304
305 if (cell.colspan > 1) {
306 $cell.attr('colspan', cell.colspan);
307 }
308 if (cell.rowspan > 1) {
309 $cell.attr('rowspan', cell.rowspan);
310 }
311 $row.append($cell);
312 }
313 $table.append($row);
314 }
315 $(GridEditor.targetElement).empty().append($table);
316 };
317
318 /**
319 * Sets the name of a certain grid element.
320 *
321 * @param {String} newName
322 * @param {Integer} col
323 * @param {Integer} row
324 *
325 * @returns {Boolean}
326 */
327 GridEditor.setName = function(newName, col, row) {
328 var cell = GridEditor.getCell(col, row);
329 if (!cell) {
330 return false;
331 }
332 cell.name = GridEditor.stripMarkup(newName);
333 return true;
334 };
335
336 /**
337 * Sets the column field for a certain grid element. This is NOT the column of the
338 * element itself.
339 *
340 * @param {Integer} newColumn
341 * @param {Integer} col
342 * @param {Integer} row
343 *
344 * @returns {Boolean}
345 */
346 GridEditor.setColumn = function(newColumn, col, row) {
347 var cell = GridEditor.getCell(col, row);
348 if (!cell) {
349 return false;
350 }
351 cell.column = parseInt(GridEditor.stripMarkup(newColumn), 10);
352 return true;
353 };
354
355 /**
356 * Creates an ExtJs Window with two input fields and shows it. On save, the data
357 * is written into the grid element.
358 *
359 * @param {Integer} col
360 * @param {Integer} row
361 *
362 * @returns {Boolean}
363 */
364 GridEditor.showOptions = function(col, row) {
365 var cell = GridEditor.getCell(col, row);
366 if (!cell) {
367 return false;
368 }
369
370 var $markup = $('<div>');
371 $markup.append(
372 '<div>' +
373 '<div class="form-group">' +
374 '<label>' + TYPO3.lang['nameHelp'] + '</label>' +
375 '<input type="text" class="t3js-grideditor-field-name form-control" name="name" value="' + (GridEditor.stripMarkup(cell.name) || '') + '">' +
376 '</div>' +
377 '<div class="form-group">' +
378 '<label>' + TYPO3.lang['columnHelp'] + '</label>' +
379 '<input type="text" class="t3js-grideditor-field-colpos form-control" name="name" value="' + (parseInt(cell.column, 10) || '') + '">' +
380 '</div>' +
381 '</div>'
382 );
383 var $modal = Modal.show(TYPO3.lang['title'], $markup, Severity.notice, [
384 {
385 text: $(this).data('button-close-text') || TYPO3.lang['button.cancel'] || 'Cancel',
386 active: true,
387 btnClass: 'btn-default',
388 name: 'cancel'
389 },
390 {
391 text: $(this).data('button-ok-text') || TYPO3.lang['button.ok'] || 'OK',
392 btnClass: 'btn-' + Severity.getCssClass(Severity.notice),
393 name: 'ok'
394 }
395 ]);
396 $modal.data('col', col);
397 $modal.data('row', row);
398 $modal.on('button.clicked', function(e) {
399 if (e.target.name === 'cancel') {
400 $(this).trigger('modal-dismiss');
401 } else if (e.target.name === 'ok') {
402 GridEditor.setName($modal.find('.t3js-grideditor-field-name').val(), $modal.data('col'), $modal.data('row'));
403 GridEditor.setColumn($modal.find('.t3js-grideditor-field-colpos').val(), $modal.data('col'), $modal.data('row'));
404 GridEditor.drawTable();
405 $(this).trigger('modal-dismiss');
406 }
407 });
408 };
409
410 /**
411 * Returns a cell element from the grid.
412 *
413 * @param {Integer} col
414 * @param {Integer} row
415 * @returns {Object}
416 */
417 GridEditor.getCell = function(col, row) {
418 if (col > GridEditor.colCount - 1) {
419 return false;
420 }
421 if (row > GridEditor.rowCount - 1) {
422 return false;
423 }
424 return GridEditor.data[row][col];
425 };
426
427 /**
428 * Checks whether a cell can span to the right or not. A cell can span to the right
429 * if it is not in the last column and if there is no cell beside it that is
430 * already overspanned by some other cell.
431 *
432 * @param {Integer} col
433 * @param {Integer} row
434 * @returns {Boolean}
435 */
436 GridEditor.cellCanSpanRight = function(col, row) {
437 if (col == GridEditor.colCount - 1) {
438 return false;
439 }
440
441 var cell = GridEditor.getCell(col, row);
442 var checkCell;
443 if (cell.rowspan > 1) {
444 for (var rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) {
445 checkCell = GridEditor.getCell(col + cell.colspan, rowIndex);
446 if (!checkCell || checkCell.spanned == true || checkCell.colspan > 1 || checkCell.rowspan > 1) {
447 return false;
448 }
449 }
450 } else {
451 checkCell = GridEditor.getCell(col + cell.colspan, row);
452 if (!checkCell || cell.spanned == true || checkCell.spanned == true || checkCell.colspan > 1 || checkCell.rowspan > 1) {
453 return false;
454 }
455 }
456
457 return true;
458 };
459
460 /**
461 * Checks whether a cell can span down or not.
462 *
463 * @param {Integer} col
464 * @param {Integer} row
465 * @returns {Boolean}
466 */
467 GridEditor.cellCanSpanDown = function(col, row) {
468 if (row == GridEditor.rowCount - 1) {
469 return false;
470 }
471
472 var cell = GridEditor.getCell(col, row);
473 var checkCell;
474 if (cell.colspan > 1) {
475 // we have to check all cells on the right side for the complete colspan
476 for (var colIndex = col; colIndex < col + cell.colspan; colIndex++) {
477 checkCell = GridEditor.getCell(colIndex, row + cell.rowspan);
478 if (!checkCell || checkCell.spanned == true || checkCell.colspan > 1 || checkCell.rowspan > 1) {
479 return false;
480 }
481 }
482 } else {
483 checkCell = GridEditor.getCell(col, row + cell.rowspan);
484 if (!checkCell || cell.spanned == true || checkCell.spanned == true || checkCell.colspan > 1 || checkCell.rowspan > 1) {
485 return false;
486 }
487 }
488
489 return true;
490 };
491
492 /**
493 * Checks if a cell can shrink to the left. It can shrink if the colspan of the
494 * cell is bigger than 1.
495 *
496 * @param {Integer} col
497 * @param {Integer} row
498 * @returns {Boolean}
499 */
500 GridEditor.cellCanShrinkLeft = function(col, row) {
501 return (GridEditor.data[row][col].colspan > 1);
502 };
503
504 /**
505 * Returns if a cell can shrink up. This is the case if a cell has at least
506 * a rowspan of 2.
507 *
508 * @param {Integer} col
509 * @param {Integer} row
510 * @returns {Boolean}
511 */
512 GridEditor.cellCanShrinkUp = function(col, row) {
513 return (GridEditor.data[row][col].rowspan > 1);
514 };
515
516 /**
517 * Adds a colspan to a grid element.
518 *
519 * @param {Integer} col
520 * @param {Integer} row
521 * @returns {Boolean}
522 */
523 GridEditor.addColspan = function(col, row) {
524 var cell = GridEditor.getCell(col, row);
525 if (!cell || !GridEditor.cellCanSpanRight(col, row)) {
526 return false;
527 }
528
529 for (var rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) {
530 GridEditor.data[rowIndex][col + cell.colspan].spanned = true;
531 }
532 cell.colspan += 1;
533 };
534
535 /**
536 * Adds a rowspan to grid element.
537 *
538 * @param {Integer} col
539 * @param {Integer} row
540 * @returns {Boolean}
541 */
542 GridEditor.addRowspan = function(col, row) {
543 var cell = GridEditor.getCell(col, row);
544 if (!cell || !GridEditor.cellCanSpanDown(col, row)) {
545 return false;
546 }
547
548 for (var colIndex = col; colIndex < col + cell.colspan; colIndex++) {
549 GridEditor.data[row + cell.rowspan][colIndex].spanned = true;
550 }
551 cell.rowspan += 1;
552 };
553
554 /**
555 * Removes a colspan from a grid element.
556 *
557 * @param {Integer} col
558 * @param {Integer} row
559 * @returns {Boolean}
560 */
561 GridEditor.removeColspan = function(col, row) {
562 var cell = GridEditor.getCell(col, row);
563 if (!cell || !GridEditor.cellCanShrinkLeft(col, row)) {
564 return false;
565 }
566
567 cell.colspan -= 1;
568 for (var rowIndex = row; rowIndex < row + cell.rowspan; rowIndex++) {
569 GridEditor.data[rowIndex][col + cell.colspan].spanned = false;
570 }
571 };
572
573 /**
574 * Removes a rowspan from a grid element.
575 *
576 * @param {Integer} col
577 * @param {Integer} row
578 * @returns {Boolean}
579 */
580 GridEditor.removeRowspan = function(col, row) {
581 var cell = GridEditor.getCell(col, row);
582 if (!cell || !GridEditor.cellCanShrinkUp(col, row)) {
583 return false;
584 }
585
586 cell.rowspan -= 1;
587 for (var colIndex = col; colIndex < col + cell.colspan; colIndex++) {
588 GridEditor.data[row + cell.rowspan][colIndex].spanned = false;
589 }
590 };
591
592 /**
593 * Exports the current grid to a TypoScript notation that can be read by the
594 * page module and is human readable.
595 *
596 * @returns {String}
597 */
598 GridEditor.export2LayoutRecord = function() {
599 var result = "backend_layout {\n\tcolCount = " + GridEditor.colCount + "\n\trowCount = " + GridEditor.rowCount + "\n\trows {\n";
600 for (var row = 0; row < GridEditor.rowCount; row++) {
601 result += "\t\t" + (row + 1) + " {\n";
602 result += "\t\t\tcolumns {\n";
603 var colIndex = 0;
604 for (var col = 0; col < GridEditor.colCount; col++) {
605 var cell = GridEditor.getCell(col, row);
606 if (cell && !cell.spanned) {
607 colIndex++;
608 result += "\t\t\t\t" + (colIndex) + " {\n";
609 result += "\t\t\t\t\tname = " + ((!cell.name) ? col + "x" + row : cell.name) + "\n";
610 if (cell.colspan > 1) {
611 result += "\t\t\t\t\tcolspan = " + cell.colspan + "\n";
612 }
613 if (cell.rowspan > 1) {
614 result += "\t\t\t\t\trowspan = " + cell.rowspan + "\n";
615 }
616 if (typeof(cell.column) === 'number') {
617 result += "\t\t\t\t\tcolPos = " + cell.column + "\n";
618 }
619 result += "\t\t\t\t}\n";
620 }
621
622 }
623 result += "\t\t\t}\n";
624 result += "\t\t}\n";
625 }
626
627 result += "\t}\n}\n";
628 return result;
629 };
630
631 /**
632 *
633 * @param {String} input
634 * @returns {*|jQuery}
635 */
636 GridEditor.stripMarkup = function(input) {
637 return $('<p>' + input + '</p>').text();
638 };
639
640 GridEditor.initialize();
641 return GridEditor;
642 });