comparison WebContent/jscripts/tiny_mce/plugins/table/editor_plugin_src.js @ 5:0be9d53a6967

editor for annotations
author dwinter
date Tue, 13 Dec 2011 17:43:46 +0100
parents
children
comparison
equal deleted inserted replaced
4:c32080f364c6 5:0be9d53a6967
1 /**
2 * editor_plugin_src.js
3 *
4 * Copyright 2009, Moxiecode Systems AB
5 * Released under LGPL License.
6 *
7 * License: http://tinymce.moxiecode.com/license
8 * Contributing: http://tinymce.moxiecode.com/contributing
9 */
10
11 (function(tinymce) {
12 var each = tinymce.each;
13
14 // Checks if the selection/caret is at the start of the specified block element
15 function isAtStart(rng, par) {
16 var doc = par.ownerDocument, rng2 = doc.createRange(), elm;
17
18 rng2.setStartBefore(par);
19 rng2.setEnd(rng.endContainer, rng.endOffset);
20
21 elm = doc.createElement('body');
22 elm.appendChild(rng2.cloneContents());
23
24 // Check for text characters of other elements that should be treated as content
25 return elm.innerHTML.replace(/<(br|img|object|embed|input|textarea)[^>]*>/gi, '-').replace(/<[^>]+>/g, '').length == 0;
26 };
27
28 function getSpanVal(td, name) {
29 return parseInt(td.getAttribute(name) || 1);
30 }
31
32 /**
33 * Table Grid class.
34 */
35 function TableGrid(table, dom, selection) {
36 var grid, startPos, endPos, selectedCell;
37
38 buildGrid();
39 selectedCell = dom.getParent(selection.getStart(), 'th,td');
40 if (selectedCell) {
41 startPos = getPos(selectedCell);
42 endPos = findEndPos();
43 selectedCell = getCell(startPos.x, startPos.y);
44 }
45
46 function cloneNode(node, children) {
47 node = node.cloneNode(children);
48 node.removeAttribute('id');
49
50 return node;
51 }
52
53 function buildGrid() {
54 var startY = 0;
55
56 grid = [];
57
58 each(['thead', 'tbody', 'tfoot'], function(part) {
59 var rows = dom.select('> ' + part + ' tr', table);
60
61 each(rows, function(tr, y) {
62 y += startY;
63
64 each(dom.select('> td, > th', tr), function(td, x) {
65 var x2, y2, rowspan, colspan;
66
67 // Skip over existing cells produced by rowspan
68 if (grid[y]) {
69 while (grid[y][x])
70 x++;
71 }
72
73 // Get col/rowspan from cell
74 rowspan = getSpanVal(td, 'rowspan');
75 colspan = getSpanVal(td, 'colspan');
76
77 // Fill out rowspan/colspan right and down
78 for (y2 = y; y2 < y + rowspan; y2++) {
79 if (!grid[y2])
80 grid[y2] = [];
81
82 for (x2 = x; x2 < x + colspan; x2++) {
83 grid[y2][x2] = {
84 part : part,
85 real : y2 == y && x2 == x,
86 elm : td,
87 rowspan : rowspan,
88 colspan : colspan
89 };
90 }
91 }
92 });
93 });
94
95 startY += rows.length;
96 });
97 };
98
99 function getCell(x, y) {
100 var row;
101
102 row = grid[y];
103 if (row)
104 return row[x];
105 };
106
107 function setSpanVal(td, name, val) {
108 if (td) {
109 val = parseInt(val);
110
111 if (val === 1)
112 td.removeAttribute(name, 1);
113 else
114 td.setAttribute(name, val, 1);
115 }
116 }
117
118 function isCellSelected(cell) {
119 return cell && (dom.hasClass(cell.elm, 'mceSelected') || cell == selectedCell);
120 };
121
122 function getSelectedRows() {
123 var rows = [];
124
125 each(table.rows, function(row) {
126 each(row.cells, function(cell) {
127 if (dom.hasClass(cell, 'mceSelected') || cell == selectedCell.elm) {
128 rows.push(row);
129 return false;
130 }
131 });
132 });
133
134 return rows;
135 };
136
137 function deleteTable() {
138 var rng = dom.createRng();
139
140 rng.setStartAfter(table);
141 rng.setEndAfter(table);
142
143 selection.setRng(rng);
144
145 dom.remove(table);
146 };
147
148 function cloneCell(cell) {
149 var formatNode;
150
151 // Clone formats
152 tinymce.walk(cell, function(node) {
153 var curNode;
154
155 if (node.nodeType == 3) {
156 each(dom.getParents(node.parentNode, null, cell).reverse(), function(node) {
157 node = cloneNode(node, false);
158
159 if (!formatNode)
160 formatNode = curNode = node;
161 else if (curNode)
162 curNode.appendChild(node);
163
164 curNode = node;
165 });
166
167 // Add something to the inner node
168 if (curNode)
169 curNode.innerHTML = tinymce.isIE ? '&nbsp;' : '<br data-mce-bogus="1" />';
170
171 return false;
172 }
173 }, 'childNodes');
174
175 cell = cloneNode(cell, false);
176 setSpanVal(cell, 'rowSpan', 1);
177 setSpanVal(cell, 'colSpan', 1);
178
179 if (formatNode) {
180 cell.appendChild(formatNode);
181 } else {
182 if (!tinymce.isIE)
183 cell.innerHTML = '<br data-mce-bogus="1" />';
184 }
185
186 return cell;
187 };
188
189 function cleanup() {
190 var rng = dom.createRng();
191
192 // Empty rows
193 each(dom.select('tr', table), function(tr) {
194 if (tr.cells.length == 0)
195 dom.remove(tr);
196 });
197
198 // Empty table
199 if (dom.select('tr', table).length == 0) {
200 rng.setStartAfter(table);
201 rng.setEndAfter(table);
202 selection.setRng(rng);
203 dom.remove(table);
204 return;
205 }
206
207 // Empty header/body/footer
208 each(dom.select('thead,tbody,tfoot', table), function(part) {
209 if (part.rows.length == 0)
210 dom.remove(part);
211 });
212
213 // Restore selection to start position if it still exists
214 buildGrid();
215
216 // Restore the selection to the closest table position
217 row = grid[Math.min(grid.length - 1, startPos.y)];
218 if (row) {
219 selection.select(row[Math.min(row.length - 1, startPos.x)].elm, true);
220 selection.collapse(true);
221 }
222 };
223
224 function fillLeftDown(x, y, rows, cols) {
225 var tr, x2, r, c, cell;
226
227 tr = grid[y][x].elm.parentNode;
228 for (r = 1; r <= rows; r++) {
229 tr = dom.getNext(tr, 'tr');
230
231 if (tr) {
232 // Loop left to find real cell
233 for (x2 = x; x2 >= 0; x2--) {
234 cell = grid[y + r][x2].elm;
235
236 if (cell.parentNode == tr) {
237 // Append clones after
238 for (c = 1; c <= cols; c++)
239 dom.insertAfter(cloneCell(cell), cell);
240
241 break;
242 }
243 }
244
245 if (x2 == -1) {
246 // Insert nodes before first cell
247 for (c = 1; c <= cols; c++)
248 tr.insertBefore(cloneCell(tr.cells[0]), tr.cells[0]);
249 }
250 }
251 }
252 };
253
254 function split() {
255 each(grid, function(row, y) {
256 each(row, function(cell, x) {
257 var colSpan, rowSpan, newCell, i;
258
259 if (isCellSelected(cell)) {
260 cell = cell.elm;
261 colSpan = getSpanVal(cell, 'colspan');
262 rowSpan = getSpanVal(cell, 'rowspan');
263
264 if (colSpan > 1 || rowSpan > 1) {
265 setSpanVal(cell, 'rowSpan', 1);
266 setSpanVal(cell, 'colSpan', 1);
267
268 // Insert cells right
269 for (i = 0; i < colSpan - 1; i++)
270 dom.insertAfter(cloneCell(cell), cell);
271
272 fillLeftDown(x, y, rowSpan - 1, colSpan);
273 }
274 }
275 });
276 });
277 };
278
279 function merge(cell, cols, rows) {
280 var startX, startY, endX, endY, x, y, startCell, endCell, cell, children, count;
281
282 // Use specified cell and cols/rows
283 if (cell) {
284 pos = getPos(cell);
285 startX = pos.x;
286 startY = pos.y;
287 endX = startX + (cols - 1);
288 endY = startY + (rows - 1);
289 } else {
290 // Use selection
291 startX = startPos.x;
292 startY = startPos.y;
293 endX = endPos.x;
294 endY = endPos.y;
295 }
296
297 // Find start/end cells
298 startCell = getCell(startX, startY);
299 endCell = getCell(endX, endY);
300
301 // Check if the cells exists and if they are of the same part for example tbody = tbody
302 if (startCell && endCell && startCell.part == endCell.part) {
303 // Split and rebuild grid
304 split();
305 buildGrid();
306
307 // Set row/col span to start cell
308 startCell = getCell(startX, startY).elm;
309 setSpanVal(startCell, 'colSpan', (endX - startX) + 1);
310 setSpanVal(startCell, 'rowSpan', (endY - startY) + 1);
311
312 // Remove other cells and add it's contents to the start cell
313 for (y = startY; y <= endY; y++) {
314 for (x = startX; x <= endX; x++) {
315 if (!grid[y] || !grid[y][x])
316 continue;
317
318 cell = grid[y][x].elm;
319
320 if (cell != startCell) {
321 // Move children to startCell
322 children = tinymce.grep(cell.childNodes);
323 each(children, function(node) {
324 startCell.appendChild(node);
325 });
326
327 // Remove bogus nodes if there is children in the target cell
328 if (children.length) {
329 children = tinymce.grep(startCell.childNodes);
330 count = 0;
331 each(children, function(node) {
332 if (node.nodeName == 'BR' && dom.getAttrib(node, 'data-mce-bogus') && count++ < children.length - 1)
333 startCell.removeChild(node);
334 });
335 }
336
337 // Remove cell
338 dom.remove(cell);
339 }
340 }
341 }
342
343 // Remove empty rows etc and restore caret location
344 cleanup();
345 }
346 };
347
348 function insertRow(before) {
349 var posY, cell, lastCell, x, rowElm, newRow, newCell, otherCell, rowSpan;
350
351 // Find first/last row
352 each(grid, function(row, y) {
353 each(row, function(cell, x) {
354 if (isCellSelected(cell)) {
355 cell = cell.elm;
356 rowElm = cell.parentNode;
357 newRow = cloneNode(rowElm, false);
358 posY = y;
359
360 if (before)
361 return false;
362 }
363 });
364
365 if (before)
366 return !posY;
367 });
368
369 for (x = 0; x < grid[0].length; x++) {
370 // Cell not found could be because of an invalid table structure
371 if (!grid[posY][x])
372 continue;
373
374 cell = grid[posY][x].elm;
375
376 if (cell != lastCell) {
377 if (!before) {
378 rowSpan = getSpanVal(cell, 'rowspan');
379 if (rowSpan > 1) {
380 setSpanVal(cell, 'rowSpan', rowSpan + 1);
381 continue;
382 }
383 } else {
384 // Check if cell above can be expanded
385 if (posY > 0 && grid[posY - 1][x]) {
386 otherCell = grid[posY - 1][x].elm;
387 rowSpan = getSpanVal(otherCell, 'rowSpan');
388 if (rowSpan > 1) {
389 setSpanVal(otherCell, 'rowSpan', rowSpan + 1);
390 continue;
391 }
392 }
393 }
394
395 // Insert new cell into new row
396 newCell = cloneCell(cell);
397 setSpanVal(newCell, 'colSpan', cell.colSpan);
398
399 newRow.appendChild(newCell);
400
401 lastCell = cell;
402 }
403 }
404
405 if (newRow.hasChildNodes()) {
406 if (!before)
407 dom.insertAfter(newRow, rowElm);
408 else
409 rowElm.parentNode.insertBefore(newRow, rowElm);
410 }
411 };
412
413 function insertCol(before) {
414 var posX, lastCell;
415
416 // Find first/last column
417 each(grid, function(row, y) {
418 each(row, function(cell, x) {
419 if (isCellSelected(cell)) {
420 posX = x;
421
422 if (before)
423 return false;
424 }
425 });
426
427 if (before)
428 return !posX;
429 });
430
431 each(grid, function(row, y) {
432 var cell, rowSpan, colSpan;
433
434 if (!row[posX])
435 return;
436
437 cell = row[posX].elm;
438 if (cell != lastCell) {
439 colSpan = getSpanVal(cell, 'colspan');
440 rowSpan = getSpanVal(cell, 'rowspan');
441
442 if (colSpan == 1) {
443 if (!before) {
444 dom.insertAfter(cloneCell(cell), cell);
445 fillLeftDown(posX, y, rowSpan - 1, colSpan);
446 } else {
447 cell.parentNode.insertBefore(cloneCell(cell), cell);
448 fillLeftDown(posX, y, rowSpan - 1, colSpan);
449 }
450 } else
451 setSpanVal(cell, 'colSpan', cell.colSpan + 1);
452
453 lastCell = cell;
454 }
455 });
456 };
457
458 function deleteCols() {
459 var cols = [];
460
461 // Get selected column indexes
462 each(grid, function(row, y) {
463 each(row, function(cell, x) {
464 if (isCellSelected(cell) && tinymce.inArray(cols, x) === -1) {
465 each(grid, function(row) {
466 var cell = row[x].elm, colSpan;
467
468 colSpan = getSpanVal(cell, 'colSpan');
469
470 if (colSpan > 1)
471 setSpanVal(cell, 'colSpan', colSpan - 1);
472 else
473 dom.remove(cell);
474 });
475
476 cols.push(x);
477 }
478 });
479 });
480
481 cleanup();
482 };
483
484 function deleteRows() {
485 var rows;
486
487 function deleteRow(tr) {
488 var nextTr, pos, lastCell;
489
490 nextTr = dom.getNext(tr, 'tr');
491
492 // Move down row spanned cells
493 each(tr.cells, function(cell) {
494 var rowSpan = getSpanVal(cell, 'rowSpan');
495
496 if (rowSpan > 1) {
497 setSpanVal(cell, 'rowSpan', rowSpan - 1);
498 pos = getPos(cell);
499 fillLeftDown(pos.x, pos.y, 1, 1);
500 }
501 });
502
503 // Delete cells
504 pos = getPos(tr.cells[0]);
505 each(grid[pos.y], function(cell) {
506 var rowSpan;
507
508 cell = cell.elm;
509
510 if (cell != lastCell) {
511 rowSpan = getSpanVal(cell, 'rowSpan');
512
513 if (rowSpan <= 1)
514 dom.remove(cell);
515 else
516 setSpanVal(cell, 'rowSpan', rowSpan - 1);
517
518 lastCell = cell;
519 }
520 });
521 };
522
523 // Get selected rows and move selection out of scope
524 rows = getSelectedRows();
525
526 // Delete all selected rows
527 each(rows.reverse(), function(tr) {
528 deleteRow(tr);
529 });
530
531 cleanup();
532 };
533
534 function cutRows() {
535 var rows = getSelectedRows();
536
537 dom.remove(rows);
538 cleanup();
539
540 return rows;
541 };
542
543 function copyRows() {
544 var rows = getSelectedRows();
545
546 each(rows, function(row, i) {
547 rows[i] = cloneNode(row, true);
548 });
549
550 return rows;
551 };
552
553 function pasteRows(rows, before) {
554 var selectedRows = getSelectedRows(),
555 targetRow = selectedRows[before ? 0 : selectedRows.length - 1],
556 targetCellCount = targetRow.cells.length;
557
558 // Calc target cell count
559 each(grid, function(row) {
560 var match;
561
562 targetCellCount = 0;
563 each(row, function(cell, x) {
564 if (cell.real)
565 targetCellCount += cell.colspan;
566
567 if (cell.elm.parentNode == targetRow)
568 match = 1;
569 });
570
571 if (match)
572 return false;
573 });
574
575 if (!before)
576 rows.reverse();
577
578 each(rows, function(row) {
579 var cellCount = row.cells.length, cell;
580
581 // Remove col/rowspans
582 for (i = 0; i < cellCount; i++) {
583 cell = row.cells[i];
584 setSpanVal(cell, 'colSpan', 1);
585 setSpanVal(cell, 'rowSpan', 1);
586 }
587
588 // Needs more cells
589 for (i = cellCount; i < targetCellCount; i++)
590 row.appendChild(cloneCell(row.cells[cellCount - 1]));
591
592 // Needs less cells
593 for (i = targetCellCount; i < cellCount; i++)
594 dom.remove(row.cells[i]);
595
596 // Add before/after
597 if (before)
598 targetRow.parentNode.insertBefore(row, targetRow);
599 else
600 dom.insertAfter(row, targetRow);
601 });
602 };
603
604 function getPos(target) {
605 var pos;
606
607 each(grid, function(row, y) {
608 each(row, function(cell, x) {
609 if (cell.elm == target) {
610 pos = {x : x, y : y};
611 return false;
612 }
613 });
614
615 return !pos;
616 });
617
618 return pos;
619 };
620
621 function setStartCell(cell) {
622 startPos = getPos(cell);
623 };
624
625 function findEndPos() {
626 var pos, maxX, maxY;
627
628 maxX = maxY = 0;
629
630 each(grid, function(row, y) {
631 each(row, function(cell, x) {
632 var colSpan, rowSpan;
633
634 if (isCellSelected(cell)) {
635 cell = grid[y][x];
636
637 if (x > maxX)
638 maxX = x;
639
640 if (y > maxY)
641 maxY = y;
642
643 if (cell.real) {
644 colSpan = cell.colspan - 1;
645 rowSpan = cell.rowspan - 1;
646
647 if (colSpan) {
648 if (x + colSpan > maxX)
649 maxX = x + colSpan;
650 }
651
652 if (rowSpan) {
653 if (y + rowSpan > maxY)
654 maxY = y + rowSpan;
655 }
656 }
657 }
658 });
659 });
660
661 return {x : maxX, y : maxY};
662 };
663
664 function setEndCell(cell) {
665 var startX, startY, endX, endY, maxX, maxY, colSpan, rowSpan;
666
667 endPos = getPos(cell);
668
669 if (startPos && endPos) {
670 // Get start/end positions
671 startX = Math.min(startPos.x, endPos.x);
672 startY = Math.min(startPos.y, endPos.y);
673 endX = Math.max(startPos.x, endPos.x);
674 endY = Math.max(startPos.y, endPos.y);
675
676 // Expand end positon to include spans
677 maxX = endX;
678 maxY = endY;
679
680 // Expand startX
681 for (y = startY; y <= maxY; y++) {
682 cell = grid[y][startX];
683
684 if (!cell.real) {
685 if (startX - (cell.colspan - 1) < startX)
686 startX -= cell.colspan - 1;
687 }
688 }
689
690 // Expand startY
691 for (x = startX; x <= maxX; x++) {
692 cell = grid[startY][x];
693
694 if (!cell.real) {
695 if (startY - (cell.rowspan - 1) < startY)
696 startY -= cell.rowspan - 1;
697 }
698 }
699
700 // Find max X, Y
701 for (y = startY; y <= endY; y++) {
702 for (x = startX; x <= endX; x++) {
703 cell = grid[y][x];
704
705 if (cell.real) {
706 colSpan = cell.colspan - 1;
707 rowSpan = cell.rowspan - 1;
708
709 if (colSpan) {
710 if (x + colSpan > maxX)
711 maxX = x + colSpan;
712 }
713
714 if (rowSpan) {
715 if (y + rowSpan > maxY)
716 maxY = y + rowSpan;
717 }
718 }
719 }
720 }
721
722 // Remove current selection
723 dom.removeClass(dom.select('td.mceSelected,th.mceSelected'), 'mceSelected');
724
725 // Add new selection
726 for (y = startY; y <= maxY; y++) {
727 for (x = startX; x <= maxX; x++) {
728 if (grid[y][x])
729 dom.addClass(grid[y][x].elm, 'mceSelected');
730 }
731 }
732 }
733 };
734
735 // Expose to public
736 tinymce.extend(this, {
737 deleteTable : deleteTable,
738 split : split,
739 merge : merge,
740 insertRow : insertRow,
741 insertCol : insertCol,
742 deleteCols : deleteCols,
743 deleteRows : deleteRows,
744 cutRows : cutRows,
745 copyRows : copyRows,
746 pasteRows : pasteRows,
747 getPos : getPos,
748 setStartCell : setStartCell,
749 setEndCell : setEndCell
750 });
751 };
752
753 tinymce.create('tinymce.plugins.TablePlugin', {
754 init : function(ed, url) {
755 var winMan, clipboardRows, hasCellSelection = true; // Might be selected cells on reload
756
757 function createTableGrid(node) {
758 var selection = ed.selection, tblElm = ed.dom.getParent(node || selection.getNode(), 'table');
759
760 if (tblElm)
761 return new TableGrid(tblElm, ed.dom, selection);
762 };
763
764 function cleanup() {
765 // Restore selection possibilities
766 ed.getBody().style.webkitUserSelect = '';
767
768 if (hasCellSelection) {
769 ed.dom.removeClass(ed.dom.select('td.mceSelected,th.mceSelected'), 'mceSelected');
770 hasCellSelection = false;
771 }
772 };
773
774 // Register buttons
775 each([
776 ['table', 'table.desc', 'mceInsertTable', true],
777 ['delete_table', 'table.del', 'mceTableDelete'],
778 ['delete_col', 'table.delete_col_desc', 'mceTableDeleteCol'],
779 ['delete_row', 'table.delete_row_desc', 'mceTableDeleteRow'],
780 ['col_after', 'table.col_after_desc', 'mceTableInsertColAfter'],
781 ['col_before', 'table.col_before_desc', 'mceTableInsertColBefore'],
782 ['row_after', 'table.row_after_desc', 'mceTableInsertRowAfter'],
783 ['row_before', 'table.row_before_desc', 'mceTableInsertRowBefore'],
784 ['row_props', 'table.row_desc', 'mceTableRowProps', true],
785 ['cell_props', 'table.cell_desc', 'mceTableCellProps', true],
786 ['split_cells', 'table.split_cells_desc', 'mceTableSplitCells', true],
787 ['merge_cells', 'table.merge_cells_desc', 'mceTableMergeCells', true]
788 ], function(c) {
789 ed.addButton(c[0], {title : c[1], cmd : c[2], ui : c[3]});
790 });
791
792 // Select whole table is a table border is clicked
793 if (!tinymce.isIE) {
794 ed.onClick.add(function(ed, e) {
795 e = e.target;
796
797 if (e.nodeName === 'TABLE') {
798 ed.selection.select(e);
799 ed.nodeChanged();
800 }
801 });
802 }
803
804 ed.onPreProcess.add(function(ed, args) {
805 var nodes, i, node, dom = ed.dom, value;
806
807 nodes = dom.select('table', args.node);
808 i = nodes.length;
809 while (i--) {
810 node = nodes[i];
811 dom.setAttrib(node, 'data-mce-style', '');
812
813 if ((value = dom.getAttrib(node, 'width'))) {
814 dom.setStyle(node, 'width', value);
815 dom.setAttrib(node, 'width', '');
816 }
817
818 if ((value = dom.getAttrib(node, 'height'))) {
819 dom.setStyle(node, 'height', value);
820 dom.setAttrib(node, 'height', '');
821 }
822 }
823 });
824
825 // Handle node change updates
826 ed.onNodeChange.add(function(ed, cm, n) {
827 var p;
828
829 n = ed.selection.getStart();
830 p = ed.dom.getParent(n, 'td,th,caption');
831 cm.setActive('table', n.nodeName === 'TABLE' || !!p);
832
833 // Disable table tools if we are in caption
834 if (p && p.nodeName === 'CAPTION')
835 p = 0;
836
837 cm.setDisabled('delete_table', !p);
838 cm.setDisabled('delete_col', !p);
839 cm.setDisabled('delete_table', !p);
840 cm.setDisabled('delete_row', !p);
841 cm.setDisabled('col_after', !p);
842 cm.setDisabled('col_before', !p);
843 cm.setDisabled('row_after', !p);
844 cm.setDisabled('row_before', !p);
845 cm.setDisabled('row_props', !p);
846 cm.setDisabled('cell_props', !p);
847 cm.setDisabled('split_cells', !p);
848 cm.setDisabled('merge_cells', !p);
849 });
850
851 ed.onInit.add(function(ed) {
852 var startTable, startCell, dom = ed.dom, tableGrid;
853
854 winMan = ed.windowManager;
855
856 // Add cell selection logic
857 ed.onMouseDown.add(function(ed, e) {
858 if (e.button != 2) {
859 cleanup();
860
861 startCell = dom.getParent(e.target, 'td,th');
862 startTable = dom.getParent(startCell, 'table');
863 }
864 });
865
866 dom.bind(ed.getDoc(), 'mouseover', function(e) {
867 var sel, table, target = e.target;
868
869 if (startCell && (tableGrid || target != startCell) && (target.nodeName == 'TD' || target.nodeName == 'TH')) {
870 table = dom.getParent(target, 'table');
871 if (table == startTable) {
872 if (!tableGrid) {
873 tableGrid = createTableGrid(table);
874 tableGrid.setStartCell(startCell);
875
876 ed.getBody().style.webkitUserSelect = 'none';
877 }
878
879 tableGrid.setEndCell(target);
880 hasCellSelection = true;
881 }
882
883 // Remove current selection
884 sel = ed.selection.getSel();
885
886 try {
887 if (sel.removeAllRanges)
888 sel.removeAllRanges();
889 else
890 sel.empty();
891 } catch (ex) {
892 // IE9 might throw errors here
893 }
894
895 e.preventDefault();
896 }
897 });
898
899 ed.onMouseUp.add(function(ed, e) {
900 var rng, sel = ed.selection, selectedCells, nativeSel = sel.getSel(), walker, node, lastNode, endNode;
901
902 // Move selection to startCell
903 if (startCell) {
904 if (tableGrid)
905 ed.getBody().style.webkitUserSelect = '';
906
907 function setPoint(node, start) {
908 var walker = new tinymce.dom.TreeWalker(node, node);
909
910 do {
911 // Text node
912 if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) {
913 if (start)
914 rng.setStart(node, 0);
915 else
916 rng.setEnd(node, node.nodeValue.length);
917
918 return;
919 }
920
921 // BR element
922 if (node.nodeName == 'BR') {
923 if (start)
924 rng.setStartBefore(node);
925 else
926 rng.setEndBefore(node);
927
928 return;
929 }
930 } while (node = (start ? walker.next() : walker.prev()));
931 }
932
933 // Try to expand text selection as much as we can only Gecko supports cell selection
934 selectedCells = dom.select('td.mceSelected,th.mceSelected');
935 if (selectedCells.length > 0) {
936 rng = dom.createRng();
937 node = selectedCells[0];
938 endNode = selectedCells[selectedCells.length - 1];
939 rng.setStartBefore(node);
940 rng.setEndAfter(node);
941
942 setPoint(node, 1);
943 walker = new tinymce.dom.TreeWalker(node, dom.getParent(selectedCells[0], 'table'));
944
945 do {
946 if (node.nodeName == 'TD' || node.nodeName == 'TH') {
947 if (!dom.hasClass(node, 'mceSelected'))
948 break;
949
950 lastNode = node;
951 }
952 } while (node = walker.next());
953
954 setPoint(lastNode);
955
956 sel.setRng(rng);
957 }
958
959 ed.nodeChanged();
960 startCell = tableGrid = startTable = null;
961 }
962 });
963
964 ed.onKeyUp.add(function(ed, e) {
965 cleanup();
966 });
967
968 ed.onKeyDown.add(function (ed, e) {
969 fixTableCellSelection(ed);
970 });
971
972 ed.onMouseDown.add(function (ed, e) {
973 if (e.button != 2) {
974 fixTableCellSelection(ed);
975 }
976 });
977 function tableCellSelected(ed, rng, n, currentCell) {
978 // The decision of when a table cell is selected is somewhat involved. The fact that this code is
979 // required is actually a pointer to the root cause of this bug. A cell is selected when the start
980 // and end offsets are 0, the start container is a text, and the selection node is either a TR (most cases)
981 // or the parent of the table (in the case of the selection containing the last cell of a table).
982 var TEXT_NODE = 3, table = ed.dom.getParent(rng.startContainer, 'TABLE'),
983 tableParent, allOfCellSelected, tableCellSelection;
984 if (table)
985 tableParent = table.parentNode;
986 allOfCellSelected =rng.startContainer.nodeType == TEXT_NODE &&
987 rng.startOffset == 0 &&
988 rng.endOffset == 0 &&
989 currentCell &&
990 (n.nodeName=="TR" || n==tableParent);
991 tableCellSelection = (n.nodeName=="TD"||n.nodeName=="TH")&& !currentCell;
992 return allOfCellSelected || tableCellSelection;
993 // return false;
994 }
995
996 // this nasty hack is here to work around some WebKit selection bugs.
997 function fixTableCellSelection(ed) {
998 if (!tinymce.isWebKit)
999 return;
1000
1001 var rng = ed.selection.getRng();
1002 var n = ed.selection.getNode();
1003 var currentCell = ed.dom.getParent(rng.startContainer, 'TD,TH');
1004
1005 if (!tableCellSelected(ed, rng, n, currentCell))
1006 return;
1007 if (!currentCell) {
1008 currentCell=n;
1009 }
1010
1011 // Get the very last node inside the table cell
1012 var end = currentCell.lastChild;
1013 while (end.lastChild)
1014 end = end.lastChild;
1015
1016 // Select the entire table cell. Nothing outside of the table cell should be selected.
1017 rng.setEnd(end, end.nodeValue.length);
1018 ed.selection.setRng(rng);
1019 }
1020 ed.plugins.table.fixTableCellSelection=fixTableCellSelection;
1021
1022 // Add context menu
1023 if (ed && ed.plugins.contextmenu) {
1024 ed.plugins.contextmenu.onContextMenu.add(function(th, m, e) {
1025 var sm, se = ed.selection, el = se.getNode() || ed.getBody();
1026
1027 if (ed.dom.getParent(e, 'td') || ed.dom.getParent(e, 'th') || ed.dom.select('td.mceSelected,th.mceSelected').length) {
1028 m.removeAll();
1029
1030 if (el.nodeName == 'A' && !ed.dom.getAttrib(el, 'name')) {
1031 m.add({title : 'advanced.link_desc', icon : 'link', cmd : ed.plugins.advlink ? 'mceAdvLink' : 'mceLink', ui : true});
1032 m.add({title : 'advanced.unlink_desc', icon : 'unlink', cmd : 'UnLink'});
1033 m.addSeparator();
1034 }
1035
1036 if (el.nodeName == 'IMG' && el.className.indexOf('mceItem') == -1) {
1037 m.add({title : 'advanced.image_desc', icon : 'image', cmd : ed.plugins.advimage ? 'mceAdvImage' : 'mceImage', ui : true});
1038 m.addSeparator();
1039 }
1040
1041 m.add({title : 'table.desc', icon : 'table', cmd : 'mceInsertTable', value : {action : 'insert'}});
1042 m.add({title : 'table.props_desc', icon : 'table_props', cmd : 'mceInsertTable'});
1043 m.add({title : 'table.del', icon : 'delete_table', cmd : 'mceTableDelete'});
1044 m.addSeparator();
1045
1046 // Cell menu
1047 sm = m.addMenu({title : 'table.cell'});
1048 sm.add({title : 'table.cell_desc', icon : 'cell_props', cmd : 'mceTableCellProps'});
1049 sm.add({title : 'table.split_cells_desc', icon : 'split_cells', cmd : 'mceTableSplitCells'});
1050 sm.add({title : 'table.merge_cells_desc', icon : 'merge_cells', cmd : 'mceTableMergeCells'});
1051
1052 // Row menu
1053 sm = m.addMenu({title : 'table.row'});
1054 sm.add({title : 'table.row_desc', icon : 'row_props', cmd : 'mceTableRowProps'});
1055 sm.add({title : 'table.row_before_desc', icon : 'row_before', cmd : 'mceTableInsertRowBefore'});
1056 sm.add({title : 'table.row_after_desc', icon : 'row_after', cmd : 'mceTableInsertRowAfter'});
1057 sm.add({title : 'table.delete_row_desc', icon : 'delete_row', cmd : 'mceTableDeleteRow'});
1058 sm.addSeparator();
1059 sm.add({title : 'table.cut_row_desc', icon : 'cut', cmd : 'mceTableCutRow'});
1060 sm.add({title : 'table.copy_row_desc', icon : 'copy', cmd : 'mceTableCopyRow'});
1061 sm.add({title : 'table.paste_row_before_desc', icon : 'paste', cmd : 'mceTablePasteRowBefore'}).setDisabled(!clipboardRows);
1062 sm.add({title : 'table.paste_row_after_desc', icon : 'paste', cmd : 'mceTablePasteRowAfter'}).setDisabled(!clipboardRows);
1063
1064 // Column menu
1065 sm = m.addMenu({title : 'table.col'});
1066 sm.add({title : 'table.col_before_desc', icon : 'col_before', cmd : 'mceTableInsertColBefore'});
1067 sm.add({title : 'table.col_after_desc', icon : 'col_after', cmd : 'mceTableInsertColAfter'});
1068 sm.add({title : 'table.delete_col_desc', icon : 'delete_col', cmd : 'mceTableDeleteCol'});
1069 } else
1070 m.add({title : 'table.desc', icon : 'table', cmd : 'mceInsertTable'});
1071 });
1072 }
1073
1074 // Fix to allow navigating up and down in a table in WebKit browsers.
1075 if (tinymce.isWebKit) {
1076 function moveSelection(ed, e) {
1077 var VK = tinymce.VK;
1078 var key = e.keyCode;
1079
1080 function handle(upBool, sourceNode, event) {
1081 var siblingDirection = upBool ? 'previousSibling' : 'nextSibling';
1082 var currentRow = ed.dom.getParent(sourceNode, 'tr');
1083 var siblingRow = currentRow[siblingDirection];
1084
1085 if (siblingRow) {
1086 moveCursorToRow(ed, sourceNode, siblingRow, upBool);
1087 tinymce.dom.Event.cancel(event);
1088 return true;
1089 } else {
1090 var tableNode = ed.dom.getParent(currentRow, 'table');
1091 var middleNode = currentRow.parentNode;
1092 var parentNodeName = middleNode.nodeName.toLowerCase();
1093 if (parentNodeName === 'tbody' || parentNodeName === (upBool ? 'tfoot' : 'thead')) {
1094 var targetParent = getTargetParent(upBool, tableNode, middleNode, 'tbody');
1095 if (targetParent !== null) {
1096 return moveToRowInTarget(upBool, targetParent, sourceNode, event);
1097 }
1098 }
1099 return escapeTable(upBool, currentRow, siblingDirection, tableNode, event);
1100 }
1101 }
1102
1103 function getTargetParent(upBool, topNode, secondNode, nodeName) {
1104 var tbodies = ed.dom.select('>' + nodeName, topNode);
1105 var position = tbodies.indexOf(secondNode);
1106 if (upBool && position === 0 || !upBool && position === tbodies.length - 1) {
1107 return getFirstHeadOrFoot(upBool, topNode);
1108 } else if (position === -1) {
1109 var topOrBottom = secondNode.tagName.toLowerCase() === 'thead' ? 0 : tbodies.length - 1;
1110 return tbodies[topOrBottom];
1111 } else {
1112 return tbodies[position + (upBool ? -1 : 1)];
1113 }
1114 }
1115
1116 function getFirstHeadOrFoot(upBool, parent) {
1117 var tagName = upBool ? 'thead' : 'tfoot';
1118 var headOrFoot = ed.dom.select('>' + tagName, parent);
1119 return headOrFoot.length !== 0 ? headOrFoot[0] : null;
1120 }
1121
1122 function moveToRowInTarget(upBool, targetParent, sourceNode, event) {
1123 var targetRow = getChildForDirection(targetParent, upBool);
1124 targetRow && moveCursorToRow(ed, sourceNode, targetRow, upBool);
1125 tinymce.dom.Event.cancel(event);
1126 return true;
1127 }
1128
1129 function escapeTable(upBool, currentRow, siblingDirection, table, event) {
1130 var tableSibling = table[siblingDirection];
1131 if (tableSibling) {
1132 moveCursorToStartOfElement(tableSibling);
1133 return true;
1134 } else {
1135 var parentCell = ed.dom.getParent(table, 'td,th');
1136 if (parentCell) {
1137 return handle(upBool, parentCell, event);
1138 } else {
1139 var backUpSibling = getChildForDirection(currentRow, !upBool);
1140 moveCursorToStartOfElement(backUpSibling);
1141 return tinymce.dom.Event.cancel(event);
1142 }
1143 }
1144 }
1145
1146 function getChildForDirection(parent, up) {
1147 return parent && parent[up ? 'lastChild' : 'firstChild'];
1148 }
1149
1150 function moveCursorToStartOfElement(n) {
1151 ed.selection.setCursorLocation(n, 0);
1152 }
1153
1154 function isVerticalMovement() {
1155 return key == VK.UP || key == VK.DOWN;
1156 }
1157
1158 function isInTable(ed) {
1159 var node = ed.selection.getNode();
1160 var currentRow = ed.dom.getParent(node, 'tr');
1161 return currentRow !== null;
1162 }
1163
1164 function columnIndex(column) {
1165 var colIndex = 0;
1166 var c = column;
1167 while (c.previousSibling) {
1168 c = c.previousSibling;
1169 colIndex = colIndex + getSpanVal(c, "colspan");
1170 }
1171 return colIndex;
1172 }
1173
1174 function findColumn(rowElement, columnIndex) {
1175 var c = 0;
1176 var r = 0;
1177 each(rowElement.children, function(cell, i) {
1178 c = c + getSpanVal(cell, "colspan");
1179 r = i;
1180 if (c > columnIndex)
1181 return false;
1182 });
1183 return r;
1184 }
1185
1186 function moveCursorToRow(ed, node, row, upBool) {
1187 var srcColumnIndex = columnIndex(ed.dom.getParent(node, 'td,th'));
1188 var tgtColumnIndex = findColumn(row, srcColumnIndex);
1189 var tgtNode = row.childNodes[tgtColumnIndex];
1190 var rowCellTarget = getChildForDirection(tgtNode, upBool);
1191 moveCursorToStartOfElement(rowCellTarget || tgtNode);
1192 }
1193
1194 function shouldFixCaret(preBrowserNode) {
1195 var newNode = ed.selection.getNode();
1196 var newParent = ed.dom.getParent(newNode, 'td,th');
1197 var oldParent = ed.dom.getParent(preBrowserNode, 'td,th');
1198 return newParent && newParent !== oldParent && checkSameParentTable(newParent, oldParent)
1199 }
1200
1201 function checkSameParentTable(nodeOne, NodeTwo) {
1202 return ed.dom.getParent(nodeOne, 'TABLE') === ed.dom.getParent(NodeTwo, 'TABLE');
1203 }
1204
1205 if (isVerticalMovement() && isInTable(ed)) {
1206 var preBrowserNode = ed.selection.getNode();
1207 setTimeout(function() {
1208 if (shouldFixCaret(preBrowserNode)) {
1209 handle(!e.shiftKey && key === VK.UP, preBrowserNode, e);
1210 }
1211 }, 0);
1212 }
1213 }
1214
1215 ed.onKeyDown.add(moveSelection);
1216 }
1217
1218 // Fixes an issue on Gecko where it's impossible to place the caret behind a table
1219 // This fix will force a paragraph element after the table but only when the forced_root_block setting is enabled
1220 if (!tinymce.isIE) {
1221 function fixTableCaretPos() {
1222 var last;
1223
1224 // Skip empty text nodes form the end
1225 for (last = ed.getBody().lastChild; last && last.nodeType == 3 && !last.nodeValue.length; last = last.previousSibling) ;
1226
1227 if (last && last.nodeName == 'TABLE')
1228 ed.dom.add(ed.getBody(), 'p', null, '<br mce_bogus="1" />');
1229 };
1230
1231 // Fixes an bug where it's impossible to place the caret before a table in Gecko
1232 // this fix solves it by detecting when the caret is at the beginning of such a table
1233 // and then manually moves the caret infront of the table
1234 if (tinymce.isGecko) {
1235 ed.onKeyDown.add(function(ed, e) {
1236 var rng, table, dom = ed.dom;
1237
1238 // On gecko it's not possible to place the caret before a table
1239 if (e.keyCode == 37 || e.keyCode == 38) {
1240 rng = ed.selection.getRng();
1241 table = dom.getParent(rng.startContainer, 'table');
1242
1243 if (table && ed.getBody().firstChild == table) {
1244 if (isAtStart(rng, table)) {
1245 rng = dom.createRng();
1246
1247 rng.setStartBefore(table);
1248 rng.setEndBefore(table);
1249
1250 ed.selection.setRng(rng);
1251
1252 e.preventDefault();
1253 }
1254 }
1255 }
1256 });
1257 }
1258
1259 ed.onKeyUp.add(fixTableCaretPos);
1260 ed.onSetContent.add(fixTableCaretPos);
1261 ed.onVisualAid.add(fixTableCaretPos);
1262
1263 ed.onPreProcess.add(function(ed, o) {
1264 var last = o.node.lastChild;
1265
1266 if (last && last.childNodes.length == 1 && last.firstChild.nodeName == 'BR')
1267 ed.dom.remove(last);
1268 });
1269
1270 fixTableCaretPos();
1271 ed.startContent = ed.getContent({format : 'raw'});
1272 }
1273 });
1274
1275 // Register action commands
1276 each({
1277 mceTableSplitCells : function(grid) {
1278 grid.split();
1279 },
1280
1281 mceTableMergeCells : function(grid) {
1282 var rowSpan, colSpan, cell;
1283
1284 cell = ed.dom.getParent(ed.selection.getNode(), 'th,td');
1285 if (cell) {
1286 rowSpan = cell.rowSpan;
1287 colSpan = cell.colSpan;
1288 }
1289
1290 if (!ed.dom.select('td.mceSelected,th.mceSelected').length) {
1291 winMan.open({
1292 url : url + '/merge_cells.htm',
1293 width : 240 + parseInt(ed.getLang('table.merge_cells_delta_width', 0)),
1294 height : 110 + parseInt(ed.getLang('table.merge_cells_delta_height', 0)),
1295 inline : 1
1296 }, {
1297 rows : rowSpan,
1298 cols : colSpan,
1299 onaction : function(data) {
1300 grid.merge(cell, data.cols, data.rows);
1301 },
1302 plugin_url : url
1303 });
1304 } else
1305 grid.merge();
1306 },
1307
1308 mceTableInsertRowBefore : function(grid) {
1309 grid.insertRow(true);
1310 },
1311
1312 mceTableInsertRowAfter : function(grid) {
1313 grid.insertRow();
1314 },
1315
1316 mceTableInsertColBefore : function(grid) {
1317 grid.insertCol(true);
1318 },
1319
1320 mceTableInsertColAfter : function(grid) {
1321 grid.insertCol();
1322 },
1323
1324 mceTableDeleteCol : function(grid) {
1325 grid.deleteCols();
1326 },
1327
1328 mceTableDeleteRow : function(grid) {
1329 grid.deleteRows();
1330 },
1331
1332 mceTableCutRow : function(grid) {
1333 clipboardRows = grid.cutRows();
1334 },
1335
1336 mceTableCopyRow : function(grid) {
1337 clipboardRows = grid.copyRows();
1338 },
1339
1340 mceTablePasteRowBefore : function(grid) {
1341 grid.pasteRows(clipboardRows, true);
1342 },
1343
1344 mceTablePasteRowAfter : function(grid) {
1345 grid.pasteRows(clipboardRows);
1346 },
1347
1348 mceTableDelete : function(grid) {
1349 grid.deleteTable();
1350 }
1351 }, function(func, name) {
1352 ed.addCommand(name, function() {
1353 var grid = createTableGrid();
1354
1355 if (grid) {
1356 func(grid);
1357 ed.execCommand('mceRepaint');
1358 cleanup();
1359 }
1360 });
1361 });
1362
1363 // Register dialog commands
1364 each({
1365 mceInsertTable : function(val) {
1366 winMan.open({
1367 url : url + '/table.htm',
1368 width : 400 + parseInt(ed.getLang('table.table_delta_width', 0)),
1369 height : 320 + parseInt(ed.getLang('table.table_delta_height', 0)),
1370 inline : 1
1371 }, {
1372 plugin_url : url,
1373 action : val ? val.action : 0
1374 });
1375 },
1376
1377 mceTableRowProps : function() {
1378 winMan.open({
1379 url : url + '/row.htm',
1380 width : 400 + parseInt(ed.getLang('table.rowprops_delta_width', 0)),
1381 height : 295 + parseInt(ed.getLang('table.rowprops_delta_height', 0)),
1382 inline : 1
1383 }, {
1384 plugin_url : url
1385 });
1386 },
1387
1388 mceTableCellProps : function() {
1389 winMan.open({
1390 url : url + '/cell.htm',
1391 width : 400 + parseInt(ed.getLang('table.cellprops_delta_width', 0)),
1392 height : 295 + parseInt(ed.getLang('table.cellprops_delta_height', 0)),
1393 inline : 1
1394 }, {
1395 plugin_url : url
1396 });
1397 }
1398 }, function(func, name) {
1399 ed.addCommand(name, function(ui, val) {
1400 func(val);
1401 });
1402 });
1403 }
1404 });
1405
1406 // Register plugin
1407 tinymce.PluginManager.add('table', tinymce.plugins.TablePlugin);
1408 })(tinymce);