|
1 /** |
|
2 * Quirks.js |
|
3 * |
|
4 * Copyright, Moxiecode Systems AB |
|
5 * Released under LGPL License. |
|
6 * |
|
7 * License: http://www.tinymce.com/license |
|
8 * Contributing: http://www.tinymce.com/contributing |
|
9 */ |
|
10 |
|
11 /** |
|
12 * This class includes fixes for various browser quirks. |
|
13 * |
|
14 * @class tinymce.tableplugin.Quirks |
|
15 * @private |
|
16 */ |
|
17 define("tinymce/tableplugin/Quirks", [ |
|
18 "tinymce/util/VK", |
|
19 "tinymce/Env", |
|
20 "tinymce/util/Tools" |
|
21 ], function(VK, Env, Tools) { |
|
22 var each = Tools.each; |
|
23 |
|
24 function getSpanVal(td, name) { |
|
25 return parseInt(td.getAttribute(name) || 1, 10); |
|
26 } |
|
27 |
|
28 return function(editor) { |
|
29 /** |
|
30 * Fixed caret movement around tables on WebKit. |
|
31 */ |
|
32 function moveWebKitSelection() { |
|
33 function eventHandler(e) { |
|
34 var key = e.keyCode; |
|
35 |
|
36 function handle(upBool, sourceNode) { |
|
37 var siblingDirection = upBool ? 'previousSibling' : 'nextSibling'; |
|
38 var currentRow = editor.dom.getParent(sourceNode, 'tr'); |
|
39 var siblingRow = currentRow[siblingDirection]; |
|
40 |
|
41 if (siblingRow) { |
|
42 moveCursorToRow(editor, sourceNode, siblingRow, upBool); |
|
43 e.preventDefault(); |
|
44 return true; |
|
45 } else { |
|
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); |
|
53 } |
|
54 } |
|
55 return escapeTable(upBool, currentRow, siblingDirection, tableNode); |
|
56 } |
|
57 } |
|
58 |
|
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]; |
|
67 } else { |
|
68 return tbodies[position + (upBool ? -1 : 1)]; |
|
69 } |
|
70 } |
|
71 |
|
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; |
|
76 } |
|
77 |
|
78 function moveToRowInTarget(upBool, targetParent, sourceNode) { |
|
79 var targetRow = getChildForDirection(targetParent, upBool); |
|
80 |
|
81 if (targetRow) { |
|
82 moveCursorToRow(editor, sourceNode, targetRow, upBool); |
|
83 } |
|
84 |
|
85 e.preventDefault(); |
|
86 return true; |
|
87 } |
|
88 |
|
89 function escapeTable(upBool, currentRow, siblingDirection, table) { |
|
90 var tableSibling = table[siblingDirection]; |
|
91 |
|
92 if (tableSibling) { |
|
93 moveCursorToStartOfElement(tableSibling); |
|
94 return true; |
|
95 } else { |
|
96 var parentCell = editor.dom.getParent(table, 'td,th'); |
|
97 if (parentCell) { |
|
98 return handle(upBool, parentCell, e); |
|
99 } else { |
|
100 var backUpSibling = getChildForDirection(currentRow, !upBool); |
|
101 moveCursorToStartOfElement(backUpSibling); |
|
102 e.preventDefault(); |
|
103 return false; |
|
104 } |
|
105 } |
|
106 } |
|
107 |
|
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; |
|
112 } |
|
113 |
|
114 function moveCursorToStartOfElement(n) { |
|
115 editor.selection.setCursorLocation(n, 0); |
|
116 } |
|
117 |
|
118 function isVerticalMovement() { |
|
119 return key == VK.UP || key == VK.DOWN; |
|
120 } |
|
121 |
|
122 function isInTable(editor) { |
|
123 var node = editor.selection.getNode(); |
|
124 var currentRow = editor.dom.getParent(node, 'tr'); |
|
125 return currentRow !== null; |
|
126 } |
|
127 |
|
128 function columnIndex(column) { |
|
129 var colIndex = 0; |
|
130 var c = column; |
|
131 while (c.previousSibling) { |
|
132 c = c.previousSibling; |
|
133 colIndex = colIndex + getSpanVal(c, "colspan"); |
|
134 } |
|
135 return colIndex; |
|
136 } |
|
137 |
|
138 function findColumn(rowElement, columnIndex) { |
|
139 var c = 0, r = 0; |
|
140 |
|
141 each(rowElement.children, function(cell, i) { |
|
142 c = c + getSpanVal(cell, "colspan"); |
|
143 r = i; |
|
144 if (c > columnIndex) { |
|
145 return false; |
|
146 } |
|
147 }); |
|
148 return r; |
|
149 } |
|
150 |
|
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); |
|
157 } |
|
158 |
|
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'); |
|
163 |
|
164 return newParent && newParent !== oldParent && checkSameParentTable(newParent, oldParent); |
|
165 } |
|
166 |
|
167 function checkSameParentTable(nodeOne, NodeTwo) { |
|
168 return editor.dom.getParent(nodeOne, 'TABLE') === editor.dom.getParent(NodeTwo, 'TABLE'); |
|
169 } |
|
170 |
|
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); |
|
176 } |
|
177 }, 0); |
|
178 } |
|
179 } |
|
180 |
|
181 editor.on('KeyDown', function(e) { |
|
182 eventHandler(e); |
|
183 }); |
|
184 } |
|
185 |
|
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; |
|
190 |
|
191 rng2.setStartBefore(par); |
|
192 rng2.setEnd(rng.endContainer, rng.endOffset); |
|
193 |
|
194 elm = doc.createElement('body'); |
|
195 elm.appendChild(rng2.cloneContents()); |
|
196 |
|
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; |
|
199 } |
|
200 |
|
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; |
|
206 |
|
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'); |
|
211 |
|
212 if (table && editor.getBody().firstChild == table) { |
|
213 if (isAtStart(rng, table)) { |
|
214 rng = dom.createRng(); |
|
215 |
|
216 rng.setStartBefore(table); |
|
217 rng.setEndBefore(table); |
|
218 |
|
219 editor.selection.setRng(rng); |
|
220 |
|
221 e.preventDefault(); |
|
222 } |
|
223 } |
|
224 } |
|
225 }); |
|
226 } |
|
227 |
|
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() { |
|
232 var last; |
|
233 |
|
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) { |
|
238 break; |
|
239 } |
|
240 } else if (last.nodeType == 1 && (last.tagName == 'BR' || !last.getAttribute('data-mce-bogus'))) { |
|
241 break; |
|
242 } |
|
243 } |
|
244 |
|
245 if (last && last.nodeName == 'TABLE') { |
|
246 if (editor.settings.forced_root_block) { |
|
247 editor.dom.add( |
|
248 editor.getBody(), |
|
249 editor.settings.forced_root_block, |
|
250 editor.settings.forced_root_block_attrs, |
|
251 Env.ie && Env.ie < 11 ? ' ' : '<br data-mce-bogus="1" />' |
|
252 ); |
|
253 } else { |
|
254 editor.dom.add(editor.getBody(), 'br', {'data-mce-bogus': '1'}); |
|
255 } |
|
256 } |
|
257 }); |
|
258 |
|
259 editor.on('PreProcess', function(o) { |
|
260 var last = o.node.lastChild; |
|
261 |
|
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); |
|
266 } |
|
267 }); |
|
268 } |
|
269 |
|
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; |
|
279 |
|
280 if (table) { |
|
281 tableParent = table.parentNode; |
|
282 } |
|
283 |
|
284 allOfCellSelected = rng.startContainer.nodeType == TEXT_NODE && |
|
285 rng.startOffset === 0 && |
|
286 rng.endOffset === 0 && |
|
287 currentCell && |
|
288 (n.nodeName == "TR" || n == tableParent); |
|
289 |
|
290 tableCellSelection = (n.nodeName == "TD" || n.nodeName == "TH") && !currentCell; |
|
291 |
|
292 return allOfCellSelected || tableCellSelection; |
|
293 } |
|
294 |
|
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'); |
|
299 |
|
300 if (!tableCellSelected(editor, rng, n, currentCell)) { |
|
301 return; |
|
302 } |
|
303 |
|
304 if (!currentCell) { |
|
305 currentCell = n; |
|
306 } |
|
307 |
|
308 // Get the very last node inside the table cell |
|
309 var end = currentCell.lastChild; |
|
310 while (end.lastChild) { |
|
311 end = end.lastChild; |
|
312 } |
|
313 |
|
314 // Select the entire table cell. Nothing outside of the table cell should be selected. |
|
315 if (end.nodeType == 3) { |
|
316 rng.setEnd(end, end.data.length); |
|
317 editor.selection.setRng(rng); |
|
318 } |
|
319 } |
|
320 |
|
321 editor.on('KeyDown', function() { |
|
322 fixSelection(); |
|
323 }); |
|
324 |
|
325 editor.on('MouseDown', function(e) { |
|
326 if (e.button != 2) { |
|
327 fixSelection(); |
|
328 } |
|
329 }); |
|
330 } |
|
331 |
|
332 /** |
|
333 * Delete table if all cells are selected. |
|
334 */ |
|
335 function deleteTable() { |
|
336 editor.on('keydown', function(e) { |
|
337 if ((e.keyCode == VK.DELETE || e.keyCode == VK.BACKSPACE) && !e.isDefaultPrevented()) { |
|
338 var table = editor.dom.getParent(editor.selection.getStart(), 'table'); |
|
339 |
|
340 if (table) { |
|
341 var cells = editor.dom.select('td,th', table), i = cells.length; |
|
342 while (i--) { |
|
343 if (!editor.dom.hasClass(cells[i], 'mce-item-selected')) { |
|
344 return; |
|
345 } |
|
346 } |
|
347 |
|
348 e.preventDefault(); |
|
349 editor.execCommand('mceTableDelete'); |
|
350 } |
|
351 } |
|
352 }); |
|
353 } |
|
354 |
|
355 deleteTable(); |
|
356 |
|
357 if (Env.webkit) { |
|
358 moveWebKitSelection(); |
|
359 fixTableCellSelection(); |
|
360 } |
|
361 |
|
362 if (Env.gecko) { |
|
363 fixBeforeTableCaretBug(); |
|
364 fixTableCaretPos(); |
|
365 } |
|
366 |
|
367 if (Env.ie > 10) { |
|
368 fixBeforeTableCaretBug(); |
|
369 fixTableCaretPos(); |
|
370 } |
|
371 }; |
|
372 }); |