177ab353126afdc54026ca13fe537b1bb4e2ff62
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / htmlarea / plugins / CopyPaste / copy-paste.js
1 /***************************************************************
2 * Copyright notice
3 *
4 * (c) 2008-2012 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 HTMLArea.CopyPaste = Ext.extend(HTMLArea.Plugin, {
31 /*
32 * This function gets called by the class constructor
33 */
34 configurePlugin: function (editor) {
35 /*
36 * Setting up some properties from PageTSConfig
37 */
38 this.buttonsConfiguration = this.editorConfiguration.buttons;
39 /*
40 * Registering plugin "About" information
41 */
42 var pluginInformation = {
43 version : '2.4',
44 developer : 'Stanislas Rolland',
45 developerUrl : 'http://www.sjbr.ca/',
46 copyrightOwner : 'Stanislas Rolland',
47 sponsor : this.localize('Technische Universitat Ilmenau'),
48 sponsorUrl : 'http://www.tu-ilmenau.de/',
49 license : 'GPL'
50 };
51 this.registerPluginInformation(pluginInformation);
52 /*
53 * Registering the buttons
54 */
55 Ext.iterate(this.buttonList, function (buttonId, button) {
56 var buttonConfiguration = {
57 id : buttonId,
58 tooltip : this.localize(buttonId.toLowerCase()),
59 iconCls : 'htmlarea-action-' + button[2],
60 action : 'onButtonPress',
61 context : button[0],
62 selection : button[3],
63 hotKey : button[1]
64 };
65 this.registerButton(buttonConfiguration);
66 }, this);
67 return true;
68 },
69 /*
70 * The list of buttons added by this plugin
71 */
72 buttonList: {
73 Copy : [null, 'c', 'copy', true],
74 Cut : [null, 'x', 'cut', true],
75 Paste : [null, 'v', 'paste', false]
76 },
77 /*
78 * This function gets called when the editor is generated
79 */
80 onGenerate: function () {
81 this.editor.iframe.mon(Ext.get(Ext.isIE ? this.editor.document.body : this.editor.document.documentElement), 'cut', this.cutHandler, this);
82 Ext.iterate(this.buttonList, function (buttonId, button) {
83 // Remove button from toolbar, if command is not supported
84 // Starting with Safari 5 and Chrome 6, cut and copy commands are not supported anymore by WebKit
85 if (!Ext.isGecko && !this.editor.document.queryCommandSupported(buttonId)) {
86 this.editor.toolbar.remove(buttonId);
87 }
88 // Add hot key handling if the button is not enabled in the toolbar
89 if (!this.getButton(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().get(this.editor.getSelection().createRange());
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.getSelection().selectRange(this.editor.getBookMark().moveTo(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.getSelection().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 range = this.editor.getSelection().createRange();
212 var parent = this.editor.getSelection().getParentElement();
213 if (parent.firstChild && /^(a)$/i.test(parent.firstChild.nodeName)) {
214 parent = parent.firstChild;
215 }
216 if (/^(a)$/i.test(parent.nodeName)) {
217 parent.normalize();
218 if (!parent.innerHTML || (parent.childNodes.length == 1 && /^(br)$/i.test(parent.firstChild.nodeName))) {
219 if (!Ext.isIE) {
220 var container = parent.parentNode;
221 this.editor.getDomNode().removeMarkup(parent);
222 // Opera does not render empty list items
223 if (Ext.isOpera && /^(li)$/i.test(container.nodeName) && !container.firstChild) {
224 container.innerHTML = '<br />';
225 this.editor.getSelection().selectNodeContents(container, true);
226 }
227 } else {
228 HTMLArea.DOM.removeFromParent(parent);
229 }
230 }
231 }
232 if (Ext.isWebKit) {
233 // Remove Apple's span and font tags
234 this.editor.getDomNode().cleanAppleStyleSpans(this.editor.document.body);
235 // Reset Safari selection in order to prevent insertion of span and/or font tags on next text input
236 var bookmark = this.editor.getBookMark().get(this.editor.getSelection().createRange());
237 this.editor.getSelection().selectRange(this.editor.getBookMark().moveTo(bookmark));
238 }
239 this.editor.updateToolbar();
240 },
241 /*
242 * This function gets called when a copy/cut/paste operation is to be performed
243 * This feature allows to paste a region of table cells
244 */
245 applyToTable: function (buttonId) {
246 var range = this.editor.getSelection().createRange();
247 var parent = this.editor.getSelection().getParentElement();
248 var endBlocks = this.editor.getSelection().getEndBlocks();
249 switch (buttonId) {
250 case 'Copy':
251 case 'Cut' :
252 HTMLArea.copiedCells = null;
253 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)) {
254 HTMLArea.copiedCells = this.collectCells(buttonId, endBlocks);
255 }
256 break;
257 case 'Paste':
258 if (/^(tr|td|th)$/i.test(parent.nodeName) && HTMLArea.copiedCells) {
259 return this.pasteCells(endBlocks);
260 }
261 break;
262 default:
263 break;
264 }
265 return false;
266 },
267 /*
268 * This function handles pasting of a collection of table cells
269 */
270 pasteCells: function (endBlocks) {
271 var cell = null;
272 if (Ext.isGecko) {
273 var range = this.editor.getSelection().createRange();
274 cell = range.startContainer.childNodes[range.startOffset];
275 while (cell && !HTMLArea.DOM.isBlockElement(cell)) {
276 cell = cell.parentNode;
277 }
278 }
279 if (!cell && /^(td|th)$/i.test(endBlocks.start.nodeName)) {
280 cell = endBlocks.start;
281 }
282 if (!cell) {
283 // Let the browser do it
284 return false;
285 }
286 var tableParts = ['thead', 'tbody', 'tfoot'];
287 var tablePartsIndex = { thead : 0, tbody : 1, tfoot : 2 };
288 var tablePart = cell.parentNode.parentNode;
289 var tablePartIndex = tablePartsIndex[tablePart.nodeName.toLowerCase()]
290 var rows = HTMLArea.copiedCells[tablePartIndex];
291 if (rows && rows[0]) {
292 for (var i = 0, rowIndex = cell.parentNode.sectionRowIndex-1; i < rows.length && ++rowIndex < tablePart.rows.length; ++i) {
293 var cells = rows[i];
294 if (!cells) break;
295 var row = tablePart.rows[rowIndex];
296 for (var j = 0, cellIndex = cell.cellIndex-1; j < cells.length && ++cellIndex < row.cells.length; ++j) {
297 row.cells[cellIndex].innerHTML = cells[j];
298 }
299 }
300 }
301 var table = tablePart.parentNode;
302 for (var k = tablePartIndex +1; k < 3; ++k) {
303 tablePart = table.getElementsByTagName(tableParts[k])[0];
304 if (tablePart) {
305 var rows = HTMLArea.copiedCells[k];
306 for (var i = 0; i < rows.length && i < tablePart.rows.length; ++i) {
307 var cells = rows[i];
308 if (!cells) break;
309 var row = tablePart.rows[i];
310 for (var j = 0, cellIndex = cell.cellIndex-1; j < cells.length && ++cellIndex < row.cells.length; ++j) {
311 row.cells[cellIndex].innerHTML = cells[j];
312 }
313 }
314 }
315 }
316 return true;
317 },
318 /*
319 * This function collects the selected table cells for copy/cut operations
320 */
321 collectCells: function (operation, endBlocks) {
322 var tableParts = ['thead', 'tbody', 'tfoot'];
323 var tablePartsIndex = { thead : 0, tbody : 1, tfoot : 2 };
324 var selection = this.editor.getSelection().get().selection;
325 var range, i = 0, cell, cells = null;
326 var rows = new Array();
327 for (var k = tableParts.length; --k >= 0;) {
328 rows[k] = [];
329 }
330 var row = null;
331 var cutRows = [];
332 if (Ext.isGecko) {
333 if (selection.rangeCount == 1) { // Collect the cells in the selected row
334 cells = [];
335 for (var i = 0, n = endBlocks.start.cells.length; i < n; ++i) {
336 cell = endBlocks.start.cells[i];
337 cells.push(cell.innerHTML);
338 if (operation === 'Cut') {
339 cell.innerHTML = '<br />';
340 }
341 if (operation === 'Cut') {
342 cutRows.push(endBlocks.start);
343 }
344 }
345 rows[tablePartsIndex[endBlocks.start.parentNode.nodeName.toLowerCase()]].push(cells);
346 } else {
347 try { // Collect the cells in some region of the table
348 var firstCellOfRow = false;
349 var lastCellOfRow = false;
350 while (range = selection.getRangeAt(i++)) {
351 cell = range.startContainer.childNodes[range.startOffset];
352 if (cell.parentNode != row) {
353 (cells) && rows[tablePartsIndex[row.parentNode.nodeName.toLowerCase()]].push(cells);
354 if (operation === 'Cut' && firstCellOfRow && lastCellOfRow) cutRows.push(row);
355 row = cell.parentNode;
356 cells = [];
357 firstCellOfRow = false;
358 lastCellOfRow = false;
359 }
360 cells.push(cell.innerHTML);
361 if (operation === 'Cut') {
362 cell.innerHTML = '<br />';
363 }
364 if (!cell.previousSibling) firstCellOfRow = true;
365 if (!cell.nextSibling) lastCellOfRow = true;
366 }
367 } catch(e) {
368 /* finished walking through selection */
369 }
370 try { rows[tablePartsIndex[row.parentNode.nodeName.toLowerCase()]].push(cells); } catch(e) { }
371 if (row && operation === 'Cut' && firstCellOfRow && lastCellOfRow) {
372 cutRows.push(row);
373 }
374 }
375 } else { // Internet Explorer, Safari and Opera
376 var firstRow = endBlocks.start.parentNode;
377 var lastRow = endBlocks.end.parentNode;
378 cells = [];
379 var firstCellOfRow = false;
380 var lastCellOfRow = false;
381 if (firstRow == lastRow) { // Collect the selected cells on the row
382 cell = endBlocks.start;
383 while (cell) {
384 cells.push(cell.innerHTML);
385 if (operation === 'Cut') {
386 cell.innerHTML = '';
387 }
388 if (!cell.previousSibling) firstCellOfRow = true;
389 if (!cell.nextSibling) lastCellOfRow = true;
390 if (cell == endBlocks.end) break;
391 cell = cell.nextSibling;
392 }
393 rows[tablePartsIndex[firstRow.parentNode.nodeName.toLowerCase()]].push(cells);
394 if (operation === 'Cut' && firstCellOfRow && lastCellOfRow) cutRows.push(firstRow);
395 } else { // Collect all cells on selected rows
396 row = firstRow;
397 while (row) {
398 cells = [];
399 for (var i = 0, n = row.cells.length; i < n; ++i) {
400 cells.push(row.cells[i].innerHTML);
401 if (operation === 'Cut') {
402 row.cells[i].innerHTML = '';
403 }
404 }
405 rows[tablePartsIndex[row.parentNode.nodeName.toLowerCase()]].push(cells);
406 if (operation === 'Cut') cutRows.push(row);
407 if (row == lastRow) break;
408 row = row.nextSibling;
409 }
410 }
411 }
412 for (var i = 0, n = cutRows.length; i < n; ++i) {
413 if (i == n-1) {
414 var tablePart = cutRows[i].parentNode;
415 var next = cutRows[i].nextSibling;
416 cutRows[i].parentNode.removeChild(cutRows[i]);
417 if (next) {
418 this.editor.getSelection().selectNodeContents(next.cells[0], true);
419 } else if (tablePart.parentNode.rows.length) {
420 this.editor.getSelection().selectNodeContents(tablePart.parentNode.rows[0].cells[0], true);
421 }
422 } else {
423 cutRows[i].parentNode.removeChild(cutRows[i]);
424 }
425 }
426 return rows;
427 },
428 /*
429 * This function gets called when the toolbar is updated
430 */
431 onUpdateToolbar: function (button, mode, selectionEmpty, ancestors) {
432 if (mode === 'wysiwyg' && this.editor.isEditable() && button.itemId === 'Paste') {
433 try {
434 button.setDisabled(!this.editor.document.queryCommandEnabled(button.itemId));
435 } catch(e) {
436 button.setDisabled(true);
437 }
438 }
439 },
440 /*
441 * Mozilla clipboard access exception handler
442 */
443 mozillaClipboardAccessException: function () {
444 if (InstallTrigger && this.buttonsConfiguration.paste && this.buttonsConfiguration.paste.mozillaAllowClipboardURL) {
445 TYPO3.Dialog.QuestionDialog({
446 title: this.localize('Allow-Clipboard-Helper-Add-On-Title'),
447 msg: this.localize('Allow-Clipboard-Helper-Extension'),
448 fn: this.installAllowClipboardHelperExtension,
449 scope: this
450 });
451 } else {
452 TYPO3.Dialog.QuestionDialog({
453 title: this.localize('Firefox-Security-Prefs-Question-Title'),
454 msg: this.localize('Moz-Clipboard'),
455 fn: function (button) {
456 if (button == 'yes') {
457 window.open('http://mozilla.org/editor/midasdemo/securityprefs.html');
458 }
459 }
460 });
461 if (!InstallTrigger) {
462 this.appendToLog('mozillaClipboardAccessException', 'Firefox InstallTrigger was not defined.', 'warn');
463 }
464 }
465 },
466 /*
467 * Install AllowClipboardHelperExtension
468 *
469 * @param string button: yes or no button was clicked in the dialogue
470 *
471 * @return void
472 */
473 installAllowClipboardHelperExtension: function (button) {
474 if (button == 'yes') {
475 if (InstallTrigger.enabled()) {
476 var self = this;
477 function mozillaInstallCallback(url, returnCode) {
478 if (returnCode == 0) {
479 TYPO3.Dialog.InformationDialog({
480 title: self.localize('Allow-Clipboard-Helper-Add-On-Title'),
481 msg: self.localize('Allow-Clipboard-Helper-Extension-Success')
482 });
483 } else {
484 TYPO3.Dialog.ErrorDialog({
485 title: self.localize('Allow-Clipboard-Helper-Add-On-Title'),
486 msg: self.localize('Moz-Extension-Failure')
487 });
488 self.appendToLog('installAllowClipboardHelperExtension', 'Mozilla install return code was: ' + returnCode + '.', 'warn');
489 }
490 return false;
491 }
492 var mozillaXpi = new Object();
493 mozillaXpi['AllowClipboard Helper'] = this.buttonsConfiguration.paste.mozillaAllowClipboardURL;
494 InstallTrigger.install(mozillaXpi, mozillaInstallCallback);
495 } else {
496 TYPO3.Dialog.ErrorDialog({
497 title: this.localize('Allow-Clipboard-Helper-Add-On-Title'),
498 msg: this.localize('Mozilla-Org-Install-Not-Enabled')
499 });
500 this.appendToLog('installAllowClipboardHelperExtension', 'Mozilla install was not enabled.', 'warn');
501 }
502 }
503 }
504 });