4 * Copyright, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://www.tinymce.com/license
8 * Contributing: http://www.tinymce.com/contributing
12 * This class includes fixes for various browser quirks.
14 * @class tinymce.tableplugin.Quirks
17 define("tinymce/tableplugin/Quirks", [
21 ], function(VK, Env, Tools) {
22 var each = Tools.each;
24 function getSpanVal(td, name) {
25 return parseInt(td.getAttribute(name) || 1, 10);
28 return function(editor) {
30 * Fixed caret movement around tables on WebKit.
32 function moveWebKitSelection() {
33 function eventHandler(e) {
36 function handle(upBool, sourceNode) {
37 var siblingDirection = upBool ? 'previousSibling' : 'nextSibling';
38 var currentRow = editor.dom.getParent(sourceNode, 'tr');
39 var siblingRow = currentRow[siblingDirection];
42 moveCursorToRow(editor, sourceNode, siblingRow, upBool);
46 var tableNode = editor.dom.getParent(currentRow, 'table');
47 var middleNode = currentRow.parentNode;
48 var parentNodeName = middleNode.nodeName.toLowerCase();
49 if (parentNodeName === 'tbody' || parentNodeName === (upBool ? 'tfoot' : 'thead')) {
50 var targetParent = getTargetParent(upBool, tableNode, middleNode, 'tbody');
51 if (targetParent !== null) {
52 return moveToRowInTarget(upBool, targetParent, sourceNode);
55 return escapeTable(upBool, currentRow, siblingDirection, tableNode);
59 function getTargetParent(upBool, topNode, secondNode, nodeName) {
60 var tbodies = editor.dom.select('>' + nodeName, topNode);
61 var position = tbodies.indexOf(secondNode);
62 if (upBool && position === 0 || !upBool && position === tbodies.length - 1) {
63 return getFirstHeadOrFoot(upBool, topNode);
64 } else if (position === -1) {
65 var topOrBottom = secondNode.tagName.toLowerCase() === 'thead' ? 0 : tbodies.length - 1;
66 return tbodies[topOrBottom];
68 return tbodies[position + (upBool ? -1 : 1)];
72 function getFirstHeadOrFoot(upBool, parent) {
73 var tagName = upBool ? 'thead' : 'tfoot';
74 var headOrFoot = editor.dom.select('>' + tagName, parent);
75 return headOrFoot.length !== 0 ? headOrFoot[0] : null;
78 function moveToRowInTarget(upBool, targetParent, sourceNode) {
79 var targetRow = getChildForDirection(targetParent, upBool);
82 moveCursorToRow(editor, sourceNode, targetRow, upBool);
89 function escapeTable(upBool, currentRow, siblingDirection, table) {
90 var tableSibling = table[siblingDirection];
93 moveCursorToStartOfElement(tableSibling);
96 var parentCell = editor.dom.getParent(table, 'td,th');
98 return handle(upBool, parentCell, e);
100 var backUpSibling = getChildForDirection(currentRow, !upBool);
101 moveCursorToStartOfElement(backUpSibling);
108 function getChildForDirection(parent, up) {
109 var child = parent && parent[up ? 'lastChild' : 'firstChild'];
110 // BR is not a valid table child to return in this case we return the table cell
111 return child && child.nodeName === 'BR' ? editor.dom.getParent(child, 'td,th') : child;
114 function moveCursorToStartOfElement(n) {
115 editor.selection.setCursorLocation(n, 0);
118 function isVerticalMovement() {
119 return key == VK.UP || key == VK.DOWN;
122 function isInTable(editor) {
123 var node = editor.selection.getNode();
124 var currentRow = editor.dom.getParent(node, 'tr');
125 return currentRow !== null;
128 function columnIndex(column) {
131 while (c.previousSibling) {
132 c = c.previousSibling;
133 colIndex = colIndex + getSpanVal(c, "colspan");
138 function findColumn(rowElement, columnIndex) {
141 each(rowElement.children, function(cell, i) {
142 c = c + getSpanVal(cell, "colspan");
144 if (c > columnIndex) {
151 function moveCursorToRow(ed, node, row, upBool) {
152 var srcColumnIndex = columnIndex(editor.dom.getParent(node, 'td,th'));
153 var tgtColumnIndex = findColumn(row, srcColumnIndex);
154 var tgtNode = row.childNodes[tgtColumnIndex];
155 var rowCellTarget = getChildForDirection(tgtNode, upBool);
156 moveCursorToStartOfElement(rowCellTarget || tgtNode);
159 function shouldFixCaret(preBrowserNode) {
160 var newNode = editor.selection.getNode();
161 var newParent = editor.dom.getParent(newNode, 'td,th');
162 var oldParent = editor.dom.getParent(preBrowserNode, 'td,th');
164 return newParent && newParent !== oldParent && checkSameParentTable(newParent, oldParent);
167 function checkSameParentTable(nodeOne, NodeTwo) {
168 return editor.dom.getParent(nodeOne, 'TABLE') === editor.dom.getParent(NodeTwo, 'TABLE');
171 if (isVerticalMovement() && isInTable(editor)) {
172 var preBrowserNode = editor.selection.getNode();
173 setTimeout(function() {
174 if (shouldFixCaret(preBrowserNode)) {
175 handle(!e.shiftKey && key === VK.UP, preBrowserNode, e);
181 editor.on('KeyDown', function(e) {
186 function fixBeforeTableCaretBug() {
187 // Checks if the selection/caret is at the start of the specified block element
188 function isAtStart(rng, par) {
189 var doc = par.ownerDocument, rng2 = doc.createRange(), elm;
191 rng2.setStartBefore(par);
192 rng2.setEnd(rng.endContainer, rng.endOffset);
194 elm = doc.createElement('body');
195 elm.appendChild(rng2.cloneContents());
197 // Check for text characters of other elements that should be treated as content
198 return elm.innerHTML.replace(/<(br|img|object|embed|input|textarea)[^>]*>/gi, '-').replace(/<[^>]+>/g, '').length === 0;
201 // Fixes an bug where it's impossible to place the caret before a table in Gecko
202 // this fix solves it by detecting when the caret is at the beginning of such a table
203 // and then manually moves the caret infront of the table
204 editor.on('KeyDown', function(e) {
205 var rng, table, dom = editor.dom;
207 // On gecko it's not possible to place the caret before a table
208 if (e.keyCode == 37 || e.keyCode == 38) {
209 rng = editor.selection.getRng();
210 table = dom.getParent(rng.startContainer, 'table');
212 if (table && editor.getBody().firstChild == table) {
213 if (isAtStart(rng, table)) {
214 rng = dom.createRng();
216 rng.setStartBefore(table);
217 rng.setEndBefore(table);
219 editor.selection.setRng(rng);
228 // Fixes an issue on Gecko where it's impossible to place the caret behind a table
229 // This fix will force a paragraph element after the table but only when the forced_root_block setting is enabled
230 function fixTableCaretPos() {
231 editor.on('KeyDown SetContent VisualAid', function() {
234 // Skip empty text nodes from the end
235 for (last = editor.getBody().lastChild; last; last = last.previousSibling) {
236 if (last.nodeType == 3) {
237 if (last.nodeValue.length > 0) {
240 } else if (last.nodeType == 1 && !last.getAttribute('data-mce-bogus')) {
245 if (last && last.nodeName == 'TABLE') {
246 if (editor.settings.forced_root_block) {
249 editor.settings.forced_root_block,
251 Env.ie ? ' ' : '<br data-mce-bogus="1" />'
254 editor.dom.add(editor.getBody(), 'br', {'data-mce-bogus': '1'});
259 editor.on('PreProcess', function(o) {
260 var last = o.node.lastChild;
262 if (last && (last.nodeName == "BR" || (last.childNodes.length == 1 &&
263 (last.firstChild.nodeName == 'BR' || last.firstChild.nodeValue == '\u00a0'))) &&
264 last.previousSibling && last.previousSibling.nodeName == "TABLE") {
265 editor.dom.remove(last);
270 // this nasty hack is here to work around some WebKit selection bugs.
271 function fixTableCellSelection() {
272 function tableCellSelected(ed, rng, n, currentCell) {
273 // The decision of when a table cell is selected is somewhat involved. The fact that this code is
274 // required is actually a pointer to the root cause of this bug. A cell is selected when the start
275 // and end offsets are 0, the start container is a text, and the selection node is either a TR (most cases)
276 // or the parent of the table (in the case of the selection containing the last cell of a table).
277 var TEXT_NODE = 3, table = ed.dom.getParent(rng.startContainer, 'TABLE');
278 var tableParent, allOfCellSelected, tableCellSelection;
281 tableParent = table.parentNode;
284 allOfCellSelected =rng.startContainer.nodeType == TEXT_NODE &&
285 rng.startOffset === 0 &&
286 rng.endOffset === 0 &&
288 (n.nodeName == "TR" || n == tableParent);
290 tableCellSelection = (n.nodeName == "TD" || n.nodeName == "TH") && !currentCell;
292 return allOfCellSelected || tableCellSelection;
295 function fixSelection() {
296 var rng = editor.selection.getRng();
297 var n = editor.selection.getNode();
298 var currentCell = editor.dom.getParent(rng.startContainer, 'TD,TH');
300 if (!tableCellSelected(editor, rng, n, currentCell)) {
308 // Get the very last node inside the table cell
309 var end = currentCell.lastChild;
310 while (end.lastChild) {
314 // Select the entire table cell. Nothing outside of the table cell should be selected.
315 rng.setEnd(end, end.nodeValue.length);
316 editor.selection.setRng(rng);
319 editor.on('KeyDown', function() {
323 editor.on('MouseDown', function(e) {
331 moveWebKitSelection();
332 fixTableCellSelection();
336 fixBeforeTableCaretBug();