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