61094728410e094eb92f84c40709bc3e230b4e6c
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / htmlarea / plugins / CopyPaste / copy-paste.js
1 /***************************************************************
2 * Copyright notice
3 *
4 * (c) 2008-2010 Stanislas Rolland <typo3(arobas)sjbr.ca>
5 * All rights reserved
6 *
7 * This script is part of the TYPO3 project. The TYPO3 project is
8 * free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * The GNU General Public License can be found at
14 * http://www.gnu.org/copyleft/gpl.html.
15 * A copy is found in the textfile GPL.txt and important notices to the license
16 * from the author is found in LICENSE.txt distributed with these scripts.
17 *
18 *
19 * This script is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 *
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
27 /*
28 * Copy Paste for TYPO3 htmlArea RTE
29 *
30 * TYPO3 SVN ID: $Id$
31 */
32 HTMLArea.CopyPaste = HTMLArea.Plugin.extend({
33 constructor: function(editor, pluginName) {
34 this.base(editor, pluginName);
35 },
36 /*
37 * This function gets called by the class constructor
38 */
39 configurePlugin: function (editor) {
40 /*
41 * Setting up some properties from PageTSConfig
42 */
43 this.buttonsConfiguration = this.editorConfiguration.buttons;
44 /*
45 * Registering plugin "About" information
46 */
47 var pluginInformation = {
48 version : '2.1',
49 developer : 'Stanislas Rolland',
50 developerUrl : 'http://www.sjbr.ca/',
51 copyrightOwner : 'Stanislas Rolland',
52 sponsor : this.localize('Technische Universitat Ilmenau'),
53 sponsorUrl : 'http://www.tu-ilmenau.de/',
54 license : 'GPL'
55 };
56 this.registerPluginInformation(pluginInformation);
57 /*
58 * Registering the buttons
59 */
60 Ext.iterate(this.buttonList, function (buttonId, button) {
61 var buttonConfiguration = {
62 id : buttonId,
63 tooltip : this.localize(buttonId.toLowerCase()),
64 iconCls : 'htmlarea-action-' + button[2],
65 action : 'onButtonPress',
66 context : button[0],
67 selection : button[3],
68 hotKey : button[1]
69 };
70 this.registerButton(buttonConfiguration);
71 }, this);
72 return true;
73 },
74 /*
75 * The list of buttons added by this plugin
76 */
77 buttonList: {
78 Copy : [null, 'c', 'copy', true],
79 Cut : [null, 'x', 'cut', true],
80 Paste : [null, 'v', 'paste', false]
81 },
82 /*
83 * This function gets called when the editor is generated
84 */
85 onGenerate: function () {
86 this.editor.iframe.mon(Ext.get(Ext.isIE ? this.editor.document.body : this.editor.document.documentElement), 'cut', this.cutHandler, this);
87 // Add hot key handling if the button is not enabled in the toolbar
88 Ext.iterate(this.buttonList, function (buttonId, button) {
89 if (!this.isButtonInToolbar(buttonId)) {
90 this.editor.iframe.hotKeyMap.addBinding({
91 key: button[1].toUpperCase(),
92 ctrl: true,
93 shift: false,
94 alt: false,
95 handler: this.onHotKey,
96 scope: this
97 });
98 // Ensure the hot key can be translated
99 this.editorConfiguration.hotKeyList[button[1]] = {
100 id : button[1],
101 cmd : buttonId
102 };
103 }
104 }, this);
105 },
106 /*
107 * This function gets called when a button or a hotkey was pressed.
108 *
109 * @param object editor: the editor instance
110 * @param string id: the button id or the key
111 *
112 * @return boolean false if action is completed
113 */
114 onButtonPress: function (editor, id) {
115 // Could be a button or its hotkey
116 var buttonId = this.translateHotKey(id);
117 buttonId = buttonId ? buttonId : id;
118 this.editor.focus();
119 if (!this.applyToTable(buttonId)) {
120 // If we are not handling table cells
121 switch (buttonId) {
122 case 'Copy':
123 if (buttonId == id) {
124 // If we are handling a button, not a hotkey
125 this.applyBrowserCommand(buttonId);
126 }
127 break;
128 case 'Cut' :
129 if (buttonId == id) {
130 // If we are handling a button, not a hotkey
131 this.applyBrowserCommand(buttonId);
132 }
133 // Opera will not trigger the onCut event
134 if (Ext.isOpera) {
135 this.cutHandler();
136 }
137 break;
138 case 'Paste':
139 if (buttonId == id) {
140 // If we are handling a button, not a hotkey
141 this.applyBrowserCommand(buttonId);
142 }
143 // In FF3, the paste operation will indeed trigger the onPaste event not in FF2; nor in Opera
144 if (Ext.isOpera || Ext.isGecko2) {
145 var cleaner = this.getButton('CleanWord');
146 if (cleaner) {
147 cleaner.fireEvent.defer(250, cleaner, ['click', cleaner]);
148 }
149 }
150 break;
151 default:
152 break;
153 }
154 // Stop the event if a button was handled
155 return (buttonId != id);
156 } else {
157 // The table case was handled, let the event be stopped.
158 // No cleaning required as the pasted cells are copied from the editor.
159 // However paste by Opera cannot be stopped.
160 // Revert Opera's operation as it produces invalid html anyways
161 if (Ext.isOpera) {
162 this.editor.inhibitKeyboardInput = true;
163 var bookmark = this.editor.getBookmark(this.editor._createRange(this.editor._getSelection()));
164 var html = this.editor.getInnerHTML();
165 this.revertPaste.defer(200, this, [html, bookmark]);
166 }
167 return false;
168 }
169 },
170 /*
171 * This funcion reverts the paste operation (performed by Opera)
172 */
173 revertPaste: function (html, bookmark) {
174 this.editor.setHTML(html);
175 this.editor.selectRange(this.editor.moveToBookmark(bookmark));
176 this.editor.inhibitKeyboardInput = false;
177 },
178 /*
179 * This function applies the browser command when a button is pressed
180 * In the case of hot key, the browser does it automatically
181 */
182 applyBrowserCommand: function (buttonId) {
183 try {
184 this.editor.document.execCommand(buttonId, false, null);
185 } catch (e) {
186 if (Ext.isGecko) {
187 this.mozillaClipboardAccessException();
188 }
189 }
190 },
191 /*
192 * Handler for hotkeys configured through the hotKeyMap while button not enabled in toolbar (see onGenerate above)
193 */
194 onHotKey: function (key, event) {
195 var hotKey = String.fromCharCode(key).toLowerCase();
196 // Stop the event if it was handled here
197 if (!this.onButtonPress(this, hotKey)) {
198 event.stopEvent();
199 }
200 },
201 /*
202 * This function removes any link left over by the cut operation
203 */
204 cutHandler: function (event) {
205 this.removeEmptyLink.defer(50, this);
206 },
207 /*
208 * This function unlinks any empty link left over by the cut operation
209 */
210 removeEmptyLink: function() {
211 var selection = this.editor._getSelection();
212 var range = this.editor._createRange(selection);
213 var parent = this.editor.getParentElement(selection, range);
214 if (parent.firstChild && /^(a)$/i.test(parent.firstChild.nodeName)) {
215 parent = parent.firstChild;
216 }
217 if (/^(a)$/i.test(parent.nodeName)) {
218 parent.normalize();
219 if (!parent.innerHTML || (parent.childNodes.length == 1 && /^(br)$/i.test(parent.firstChild.nodeName))) {
220 if (!Ext.isIE) {
221 var container = parent.parentNode;
222 this.editor.removeMarkup(parent);
223 // Opera does not render empty list items
224 if (Ext.isOpera && /^(li)$/i.test(container.nodeName) && !container.firstChild) {
225 container.innerHTML = '<br />';
226 this.editor.selectNodeContents(container, true);
227 }
228 } else {
229 HTMLArea.removeFromParent(parent);
230 }
231 }
232 }
233 if (Ext.isWebKit) {
234 // Remove Apple's span and font tags
235 this.editor.cleanAppleStyleSpans(this.editor.document.body);
236 // Reset Safari selection in order to prevent insertion of span and/or font tags on next text input
237 var bookmark = this.editor.getBookmark(this.editor._createRange(this.editor._getSelection()));
238 this.editor.selectRange(this.editor.moveToBookmark(bookmark));
239 }
240 this.editor.updateToolbar();
241 },
242 /*
243 * This function gets called when a copy/cut/paste operation is to be performed
244 * This feature allows to paste a region of table cells
245 */
246 applyToTable: function (buttonId) {
247 var selection = this.editor._getSelection();
248 var range = this.editor._createRange(selection);
249 var parent = this.editor.getParentElement(selection, range);
250 var endBlocks = this.editor.getEndBlocks(selection);
251 switch (buttonId) {
252 case 'Copy':
253 case 'Cut' :
254 HTMLArea.copiedCells = null;
255 var endBlocks = this.editor.getEndBlocks(selection);
256 if ((/^(tr)$/i.test(parent.nodeName) && !Ext.isIE) || (/^(td|th)$/i.test(endBlocks.start.nodeName) && /^(td|th)$/i.test(endBlocks.end.nodeName) && !Ext.isGecko && endBlocks.start != endBlocks.end)) {
257 HTMLArea.copiedCells = this.collectCells(buttonId, selection, endBlocks);
258 }
259 break;
260 case 'Paste':
261 if (/^(tr|td|th)$/i.test(parent.nodeName) && HTMLArea.copiedCells) {
262 return this.pasteCells(selection, endBlocks);
263 }
264 break;
265 default:
266 break;
267 }
268 return false;
269 },
270 /*
271 * This function handles pasting of a collection of table cells
272 */
273 pasteCells: function (selection, endBlocks) {
274 var cell = null;
275 if (Ext.isGecko) {
276 range = selection.getRangeAt(0);
277 cell = range.startContainer.childNodes[range.startOffset];
278 while (cell && !HTMLArea.isBlockElement(cell)) {
279 cell = cell.parentNode;
280 }
281 }
282 if (!cell && /^(td|th)$/i.test(endBlocks.start.nodeName)) {
283 cell = endBlocks.start;
284 }
285 if (!cell) {
286 // Let the browser do it
287 return false;
288 }
289 var tableParts = ['thead', 'tbody', 'tfoot'];
290 var tablePartsIndex = { thead : 0, tbody : 1, tfoot : 2 };
291 var tablePart = cell.parentNode.parentNode;
292 var tablePartIndex = tablePartsIndex[tablePart.nodeName.toLowerCase()]
293 var rows = HTMLArea.copiedCells[tablePartIndex];
294 if (rows && rows[0]) {
295 for (var i = 0, rowIndex = cell.parentNode.sectionRowIndex-1; i < rows.length && ++rowIndex < tablePart.rows.length; ++i) {
296 var cells = rows[i];
297 if (!cells) break;
298 var row = tablePart.rows[rowIndex];
299 for (var j = 0, cellIndex = cell.cellIndex-1; j < cells.length && ++cellIndex < row.cells.length; ++j) {
300 row.cells[cellIndex].innerHTML = cells[j];
301 }
302 }
303 }
304 var table = tablePart.parentNode;
305 for (var k = tablePartIndex +1; k < 3; ++k) {
306 tablePart = table.getElementsByTagName(tableParts[k])[0];
307 if (tablePart) {
308 var rows = HTMLArea.copiedCells[k];
309 for (var i = 0; i < rows.length && i < tablePart.rows.length; ++i) {
310 var cells = rows[i];
311 if (!cells) break;
312 var row = tablePart.rows[i];
313 for (var j = 0, cellIndex = cell.cellIndex-1; j < cells.length && ++cellIndex < row.cells.length; ++j) {
314 row.cells[cellIndex].innerHTML = cells[j];
315 }
316 }
317 }
318 }
319 return true;
320 },
321 /*
322 * This function collects the selected table cells for copy/cut operations
323 */
324 collectCells: function (operation, selection, endBlocks) {
325 var tableParts = ['thead', 'tbody', 'tfoot'];
326 var tablePartsIndex = { thead : 0, tbody : 1, tfoot : 2 };
327 var selection = this.editor._getSelection();
328 var range, i = 0, cell, cells = null;
329 var rows = new Array();
330 for (var k = tableParts.length; --k >= 0;) {
331 rows[k] = [];
332 }
333 var row = null;
334 var cutRows = [];
335 if (Ext.isGecko) {
336 if (selection.rangeCount == 1) { // Collect the cells in the selected row
337 cells = [];
338 for (var i = 0, n = endBlocks.start.cells.length; i < n; ++i) {
339 cell = endBlocks.start.cells[i];
340 cells.push(cell.innerHTML);
341 if (operation === 'Cut') {
342 cell.innerHTML = '<br />';
343 }
344 if (operation === 'Cut') {
345 cutRows.push(endBlocks.start);
346 }
347 }
348 rows[tablePartsIndex[endBlocks.start.parentNode.nodeName.toLowerCase()]].push(cells);
349 } else {
350 try { // Collect the cells in some region of the table
351 var firstCellOfRow = false;
352 var lastCellOfRow = false;
353 while (range = selection.getRangeAt(i++)) {
354 cell = range.startContainer.childNodes[range.startOffset];
355 if (cell.parentNode != row) {
356 (cells) && rows[tablePartsIndex[row.parentNode.nodeName.toLowerCase()]].push(cells);
357 if (operation === 'Cut' && firstCellOfRow && lastCellOfRow) cutRows.push(row);
358 row = cell.parentNode;
359 cells = [];
360 firstCellOfRow = false;
361 lastCellOfRow = false;
362 }
363 cells.push(cell.innerHTML);
364 if (operation === 'Cut') {
365 cell.innerHTML = '<br />';
366 }
367 if (!cell.previousSibling) firstCellOfRow = true;
368 if (!cell.nextSibling) lastCellOfRow = true;
369 }
370 } catch(e) {
371 /* finished walking through selection */
372 }
373 try { rows[tablePartsIndex[row.parentNode.nodeName.toLowerCase()]].push(cells); } catch(e) { }
374 if (row && operation === 'Cut' && firstCellOfRow && lastCellOfRow) {
375 cutRows.push(row);
376 }
377 }
378 } else { // Internet Explorer, Safari and Opera
379 var firstRow = endBlocks.start.parentNode;
380 var lastRow = endBlocks.end.parentNode;
381 cells = [];
382 var firstCellOfRow = false;
383 var lastCellOfRow = false;
384 if (firstRow == lastRow) { // Collect the selected cells on the row
385 cell = endBlocks.start;
386 while (cell) {
387 cells.push(cell.innerHTML);
388 if (operation === 'Cut') {
389 cell.innerHTML = '';
390 }
391 if (!cell.previousSibling) firstCellOfRow = true;
392 if (!cell.nextSibling) lastCellOfRow = true;
393 if (cell == endBlocks.end) break;
394 cell = cell.nextSibling;
395 }
396 rows[tablePartsIndex[firstRow.parentNode.nodeName.toLowerCase()]].push(cells);
397 if (operation === 'Cut' && firstCellOfRow && lastCellOfRow) cutRows.push(firstRow);
398 } else { // Collect all cells on selected rows
399 row = firstRow;
400 while (row) {
401 cells = [];
402 for (var i = 0, n = row.cells.length; i < n ; ++i) {
403 cells.push(row.cells[i].innerHTML);
404 if (operation === 'Cut') {
405 row.cells[i].innerHTML = '';
406 }
407 }
408 rows[tablePartsIndex[row.parentNode.nodeName.toLowerCase()]].push(cells);
409 if (operation === 'Cut') cutRows.push(row);
410 if (row == lastRow) break;
411 row = row.nextSibling;
412 }
413 }
414 }
415 for (var i = 0, n = cutRows.length; i < n; ++i) {
416 if (i == n-1) {
417 var tablePart = cutRows[i].parentNode;
418 var next = cutRows[i].nextSibling;
419 cutRows[i].parentNode.removeChild(cutRows[i]);
420 if (next) {
421 this.editor.selectNodeContents(next.cells[0], true);
422 } else if (tablePart.parentNode.rows.length) {
423 this.editor.selectNodeContents(tablePart.parentNode.rows[0].cells[0], true);
424 }
425 } else {
426 cutRows[i].parentNode.removeChild(cutRows[i]);
427 }
428 }
429 return rows;
430 },
431 /*
432 * This function gets called when the toolbar is updated
433 */
434 onUpdateToolbar: function (button, mode, selectionEmpty, ancestors) {
435 if (mode === 'wysiwyg' && this.editor.isEditable() && button.itemId === 'Paste') {
436 try {
437 button.setDisabled(!this.editor.document.queryCommandEnabled(button.itemId));
438 } catch(e) {
439 button.setDisabled(true);
440 }
441 }
442 },
443 /*
444 * Mozilla clipboard access exception handler
445 */
446 mozillaClipboardAccessException: function () {
447 if (InstallTrigger && this.buttonsConfiguration.paste && this.buttonsConfiguration.paste.mozillaAllowClipboardURL) {
448 TYPO3.Dialog.QuestionDialog({
449 title: this.localize('Allow-Clipboard-Helper-Add-On-Title'),
450 msg: this.localize('Allow-Clipboard-Helper-Extension'),
451 fn: this.installAllowClipboardHelperExtension,
452 scope: this
453 });
454 } else {
455 TYPO3.Dialog.QuestionDialog({
456 title: this.localize('Firefox-Security-Prefs-Question-Title'),
457 msg: this.localize('Moz-Clipboard'),
458 fn: function (button) {
459 if (button == 'yes') {
460 window.open('http://mozilla.org/editor/midasdemo/securityprefs.html');
461 }
462 }
463 });
464 if (!InstallTrigger) {
465 this.appendToLog('mozillaClipboardAccessException', 'Firefox InstallTrigger was not defined.');
466 }
467 }
468 },
469 /*
470 * Install AllowClipboardHelperExtension
471 *
472 * @param string button: yes or no button was clicked in the dialogue
473 *
474 * @return void
475 */
476 installAllowClipboardHelperExtension: function (button) {
477 if (button == 'yes') {
478 if (InstallTrigger.enabled()) {
479 var self = this;
480 function mozillaInstallCallback(url, returnCode) {
481 if (returnCode == 0) {
482 TYPO3.Dialog.InformationDialog({
483 title: self.localize('Allow-Clipboard-Helper-Add-On-Title'),
484 msg: self.localize('Allow-Clipboard-Helper-Extension-Success')
485 });
486 } else {
487 TYPO3.Dialog.ErrorDialog({
488 title: self.localize('Allow-Clipboard-Helper-Add-On-Title'),
489 msg: self.localize('Moz-Extension-Failure')
490 });
491 self.appendToLog('installAllowClipboardHelperExtension', 'Mozilla install return code was: ' + returnCode + '.');
492 }
493 return false;
494 }
495 var mozillaXpi = new Object();
496 mozillaXpi['AllowClipboard Helper'] = this.buttonsConfiguration.paste.mozillaAllowClipboardURL;
497 InstallTrigger.install(mozillaXpi, mozillaInstallCallback);
498 } else {
499 TYPO3.Dialog.ErrorDialog({
500 title: this.localize('Allow-Clipboard-Helper-Add-On-Title'),
501 msg: this.localize('Mozilla-Org-Install-Not-Enabled')
502 });
503 this.appendToLog('installAllowClipboardHelperExtension', 'Mozilla install was not enabled.');
504 }
505 }
506 }
507 });