1 /***************************************************************
4 * (c) 2008-2010 Stanislas Rolland <typo3(arobas)sjbr.ca>
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.
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.
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.
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
28 * Copy Paste for TYPO3 htmlArea RTE
32 HTMLArea
.CopyPaste
= HTMLArea
.Plugin
.extend({
33 constructor: function(editor
, pluginName
) {
34 this.base(editor
, pluginName
);
37 * This function gets called by the class constructor
39 configurePlugin: function (editor
) {
41 * Setting up some properties from PageTSConfig
43 this.buttonsConfiguration
= this.editorConfiguration
.buttons
;
45 * Registering plugin "About" information
47 var pluginInformation
= {
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/',
56 this.registerPluginInformation(pluginInformation
);
58 * Registering the buttons
60 Ext
.iterate(this.buttonList
, function (buttonId
, button
) {
61 var buttonConfiguration
= {
63 tooltip
: this.localize(buttonId
.toLowerCase()),
64 iconCls
: 'htmlarea-action-' + button
[2],
65 action
: 'onButtonPress',
67 selection
: button
[3],
70 this.registerButton(buttonConfiguration
);
75 * The list of buttons added by this plugin
78 Copy
: [null, 'c', 'copy', true],
79 Cut
: [null, 'x', 'cut', true],
80 Paste
: [null, 'v', 'paste', false]
83 * This function gets called when the editor is generated
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(),
95 handler
: this.onHotKey
,
98 // Ensure the hot key can be translated
99 this.editorConfiguration
.hotKeyList
[button
[1]] = {
107 * This function gets called when a button or a hotkey was pressed.
109 * @param object editor: the editor instance
110 * @param string id: the button id or the key
112 * @return boolean false if action is completed
114 onButtonPress: function (editor
, id
) {
115 // Could be a button or its hotkey
116 var buttonId
= this.translateHotKey(id
);
117 buttonId
= buttonId
? buttonId
: id
;
119 if (!this.applyToTable(buttonId
)) {
120 // If we are not handling table cells
123 if (buttonId
== id
) {
124 // If we are handling a button, not a hotkey
125 this.applyBrowserCommand(buttonId
);
129 if (buttonId
== id
) {
130 // If we are handling a button, not a hotkey
131 this.applyBrowserCommand(buttonId
);
133 // Opera will not trigger the onCut event
139 if (buttonId
== id
) {
140 // If we are handling a button, not a hotkey
141 this.applyBrowserCommand(buttonId
);
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');
147 cleaner
.fireEvent
.defer(250, cleaner
, ['click', cleaner
]);
154 // Stop the event if a button was handled
155 return (buttonId
!= id
);
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
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
]);
171 * This funcion reverts the paste operation (performed by Opera)
173 revertPaste: function (html
, bookmark
) {
174 this.editor
.setHTML(html
);
175 this.editor
.selectRange(this.editor
.moveToBookmark(bookmark
));
176 this.editor
.inhibitKeyboardInput
= false;
179 * This function applies the browser command when a button is pressed
180 * In the case of hot key, the browser does it automatically
182 applyBrowserCommand: function (buttonId
) {
184 this.editor
.document
.execCommand(buttonId
, false, null);
187 this.mozillaClipboardAccessException();
192 * Handler for hotkeys configured through the hotKeyMap while button not enabled in toolbar (see onGenerate above)
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
)) {
202 * This function removes any link left over by the cut operation
204 cutHandler: function (event
) {
205 this.removeEmptyLink
.defer(50, this);
208 * This function unlinks any empty link left over by the cut operation
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
;
217 if (/^(a)$/i.test(parent
.nodeName
)) {
219 if (!parent
.innerHTML
|| (parent
.childNodes
.length
== 1 && /^(br)$/i.test(parent
.firstChild
.nodeName
))) {
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);
229 HTMLArea
.removeFromParent(parent
);
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
));
240 this.editor
.updateToolbar();
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
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
);
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
);
261 if (/^(tr|td|th)$/i.test(parent
.nodeName
) && HTMLArea
.copiedCells
) {
262 return this.pasteCells(selection
, endBlocks
);
271 * This function handles pasting of a collection of table cells
273 pasteCells: function (selection
, endBlocks
) {
276 range
= selection
.getRangeAt(0);
277 cell
= range
.startContainer
.childNodes
[range
.startOffset
];
278 while (cell
&& !HTMLArea
.isBlockElement(cell
)) {
279 cell
= cell
.parentNode
;
282 if (!cell
&& /^(td|th)$/i.test(endBlocks
.start
.nodeName
)) {
283 cell
= endBlocks
.start
;
286 // Let the browser do it
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
) {
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
];
304 var table
= tablePart
.parentNode
;
305 for (var k
= tablePartIndex
+1; k
< 3; ++k
) {
306 tablePart
= table
.getElementsByTagName(tableParts
[k
])[0];
308 var rows
= HTMLArea
.copiedCells
[k
];
309 for (var i
= 0; i
< rows
.length
&& i
< tablePart
.rows
.length
; ++i
) {
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
];
322 * This function collects the selected table cells for copy/cut operations
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;) {
336 if (selection
.rangeCount
== 1) { // Collect the cells in the selected row
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 />';
344 if (operation
=== 'Cut') {
345 cutRows
.push(endBlocks
.start
);
348 rows
[tablePartsIndex
[endBlocks
.start
.parentNode
.nodeName
.toLowerCase()]].push(cells
);
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
;
360 firstCellOfRow
= false;
361 lastCellOfRow
= false;
363 cells
.push(cell
.innerHTML
);
364 if (operation
=== 'Cut') {
365 cell
.innerHTML
= '<br />';
367 if (!cell
.previousSibling
) firstCellOfRow
= true;
368 if (!cell
.nextSibling
) lastCellOfRow
= true;
371 /* finished walking through selection */
373 try { rows
[tablePartsIndex
[row
.parentNode
.nodeName
.toLowerCase()]].push(cells
); } catch(e
) { }
374 if (row
&& operation
=== 'Cut' && firstCellOfRow
&& lastCellOfRow
) {
378 } else { // Internet Explorer, Safari and Opera
379 var firstRow
= endBlocks
.start
.parentNode
;
380 var lastRow
= endBlocks
.end
.parentNode
;
382 var firstCellOfRow
= false;
383 var lastCellOfRow
= false;
384 if (firstRow
== lastRow
) { // Collect the selected cells on the row
385 cell
= endBlocks
.start
;
387 cells
.push(cell
.innerHTML
);
388 if (operation
=== 'Cut') {
391 if (!cell
.previousSibling
) firstCellOfRow
= true;
392 if (!cell
.nextSibling
) lastCellOfRow
= true;
393 if (cell
== endBlocks
.end
) break;
394 cell
= cell
.nextSibling
;
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
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
= '';
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
;
415 for (var i
= 0, n
= cutRows
.length
; i
< n
; ++i
) {
417 var tablePart
= cutRows
[i
].parentNode
;
418 var next
= cutRows
[i
].nextSibling
;
419 cutRows
[i
].parentNode
.removeChild(cutRows
[i
]);
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);
426 cutRows
[i
].parentNode
.removeChild(cutRows
[i
]);
432 * This function gets called when the toolbar is updated
434 onUpdateToolbar: function (button
, mode
, selectionEmpty
, ancestors
) {
435 if (mode
=== 'wysiwyg' && this.editor
.isEditable() && button
.itemId
=== 'Paste') {
437 button
.setDisabled(!this.editor
.document
.queryCommandEnabled(button
.itemId
));
439 button
.setDisabled(true);
444 * Mozilla clipboard access exception handler
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
,
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');
464 if (!InstallTrigger
) {
465 this.appendToLog('mozillaClipboardAccessException', 'Firefox InstallTrigger was not defined.');
470 * Install AllowClipboardHelperExtension
472 * @param string button: yes or no button was clicked in the dialogue
476 installAllowClipboardHelperExtension: function (button
) {
477 if (button
== 'yes') {
478 if (InstallTrigger
.enabled()) {
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')
487 TYPO3
.Dialog
.ErrorDialog({
488 title
: self
.localize('Allow-Clipboard-Helper-Add-On-Title'),
489 msg
: self
.localize('Moz-Extension-Failure')
491 self
.appendToLog('installAllowClipboardHelperExtension', 'Mozilla install return code was: ' + returnCode
+ '.');
495 var mozillaXpi
= new Object();
496 mozillaXpi
['AllowClipboard Helper'] = this.buttonsConfiguration
.paste
.mozillaAllowClipboardURL
;
497 InstallTrigger
.install(mozillaXpi
, mozillaInstallCallback
);
499 TYPO3
.Dialog
.ErrorDialog({
500 title
: this.localize('Allow-Clipboard-Helper-Add-On-Title'),
501 msg
: this.localize('Mozilla-Org-Install-Not-Enabled')
503 this.appendToLog('installAllowClipboardHelperExtension', 'Mozilla install was not enabled.');