1 /***************************************************************
4 * (c) 2012 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 * Paste as Plain Text Plugin for TYPO3 htmlArea RTE
30 HTMLArea
.PlainText
= Ext
.extend(HTMLArea
.Plugin
, {
32 * This function gets called by the class constructor
34 configurePlugin: function (editor
) {
35 this.buttonsConfiguration
= this.editorConfiguration
.buttons
;
37 * Registering plugin "About" information
39 var pluginInformation
= {
41 developer
: 'Stanislas Rolland',
42 developerUrl
: 'http://www.sjbr.ca/',
43 copyrightOwner
: 'Stanislas Rolland',
44 sponsor
: 'Otto van Bruggen',
45 sponsorUrl
: 'http://www.webspinnerij.nl',
48 this.registerPluginInformation(pluginInformation
);
50 * Registering the buttons
52 Ext
.iterate(this.buttonList
, function (buttonId
, buttonConf
) {
53 var buttonConfiguration
= {
55 tooltip
: this.localize(buttonId
+ 'Tooltip'),
56 iconCls
: 'htmlarea-action-' + buttonConf
[1],
57 action
: 'onButtonPress',
58 dialog
: buttonConf
[2]
60 if (buttonId
== 'PasteToggle' && this.buttonsConfiguration
&& this.buttonsConfiguration
[buttonConf
[0]] && this.buttonsConfiguration
[buttonConf
[0]].hidden
) {
61 buttonConfiguration
.hide
= true;
62 buttonConfiguration
.hideInContextMenu
= true;
63 buttonConfiguration
.hotKey
= null;
64 this.buttonsConfiguration
[buttonConf
[0]].hotKey
= null;
66 this.registerButton(buttonConfiguration
);
71 * The list of buttons added by this plugin
74 PasteToggle
: ['pastetoggle', 'paste-toggle', false],
75 PasteBehaviour
: ['pastebehaviour', 'paste-behaviour', true]
78 * Cleaner configurations
82 keepTags
: /^(a|p|h[0-6]|pre|address|blockquote|div|hr|br|table|thead|tbody|tfoot|caption|tr|th|td|ul|ol|dl|li|dt|dd)$/i,
83 removeAttributes
: /^(id|on*|style|class|className|lang|align|valign|bgcolor|color|border|face|.*:.*)$/i
86 keepTags
: /^(a|p|h[0-6]|pre|address|blockquote|div|hr|br|table|thead|tbody|tfoot|caption|tr|th|td|ul|ol|dl|li|dt|dd|b|bdo|big|cite|code|del|dfn|em|i|ins|kbd|label|q|samp|small|strike|strong|sub|sup|tt|u|var)$/i,
87 removeAttributes
: /^(id|on*|style|class|className|lang|align|valign|bgcolor|color|border|face|.*:.*)$/i
91 * This function gets called when the plugin is generated
93 onGenerate: function () {
95 if (this.buttonsConfiguration
&& this.buttonsConfiguration
['pastebehaviour']) {
96 this.pasteBehaviourConfiguration
= this.buttonsConfiguration
['pastebehaviour'];
99 Ext
.iterate(this.cleanerConfig
, function (behaviour
) {
100 if (this.pasteBehaviourConfiguration
&& this.pasteBehaviourConfiguration
[behaviour
]) {
101 if (this.pasteBehaviourConfiguration
[behaviour
].keepTags
) {
102 this.cleanerConfig
[behaviour
].keepTags
= new RegExp( '^(' + this.pasteBehaviourConfiguration
[behaviour
].keepTags
.split(',').join('|') + ')$', 'i');
104 if (this.pasteBehaviourConfiguration
[behaviour
].removeAttributes
) {
105 this.cleanerConfig
[behaviour
].removeAttributes
= new RegExp( '^(' + this.pasteBehaviourConfiguration
[behaviour
].removeAttributes
.split(',').join('|') + ')$', 'i');
108 this.cleaners
[behaviour
] = new HTMLArea
.DOM
.Walker(this.cleanerConfig
[behaviour
]);
111 this.currentBehaviour
= 'plainText';
112 // May be set in TYPO3 User Settings
113 if (this.buttonsConfiguration
&& this.buttonsConfiguration
['pastebehaviour'] && this.buttonsConfiguration
['pastebehaviour']['current']) {
114 this.currentBehaviour
= this.buttonsConfiguration
['pastebehaviour']['current'];
116 // Set the toggle ON, if configured
117 if (this.buttonsConfiguration
&& this.buttonsConfiguration
['pastetoggle'] && this.buttonsConfiguration
['pastetoggle'].setActiveOnRteOpen
) {
118 this.toggleButton('PasteToggle');
120 // Start monitoring paste events
121 this.editor
.iframe
.mon(Ext
.get(Ext
.isIE
? this.editor
.document
.body
: this.editor
.document
.documentElement
), 'paste', this.onPaste
, this);
124 * This function toggles the state of a button
126 * @param string buttonId: id of button to be toggled
130 toggleButton: function (buttonId
) {
132 var button
= this.getButton(buttonId
);
133 button
.setInactive(!button
.inactive
);
136 * This function gets called when a button was pressed.
138 * @param object editor: the editor instance
139 * @param string id: the button id or the key
141 * @return boolean false if action is completed
143 onButtonPress: function (editor
, id
, target
) {
144 // Could be a button or its hotkey
145 var buttonId
= this.translateHotKey(id
);
146 buttonId
= buttonId
? buttonId
: id
;
148 case 'PasteBehaviour':
149 // Open dialogue window
152 'PasteBehaviourTooltip',
153 this.getWindowDimensions(
163 this.toggleButton(buttonId
);
170 * Open the dialogue window
172 * @param string buttonId: the button id
173 * @param string title: the window title
174 * @param object dimensions: the opening dimensions of the window
178 openDialogue: function (buttonId
, title
, dimensions
) {
179 this.dialog
= new Ext
.Window({
180 title
: this.localize(title
),
181 cls
: 'htmlarea-window',
183 width
: dimensions
.width
,
185 // As of ExtJS 3.1, JS error with IE when the window is resizable
186 resizable
: !Ext
.isIE
,
187 iconCls
: this.getButton(buttonId
).iconCls
,
196 defaultType
: 'radio',
197 title
: this.getHelpTip('behaviour', title
),
205 fieldLabel
: this.getHelpTip('plainText', 'plainText'),
206 checked
: (this.currentBehaviour
=== 'plainText')
208 itemId
: 'pasteStructure',
209 fieldLabel
: this.getHelpTip('pasteStructure', 'pasteStructure'),
210 checked
: (this.currentBehaviour
=== 'pasteStructure')
212 itemId
: 'pasteFormat',
213 fieldLabel
: this.getHelpTip('pasteFormat', 'pasteFormat'),
214 checked
: (this.currentBehaviour
=== 'pasteFormat')
220 this.buildButtonConfig('OK', this.onOK
)
226 * Handler invoked when the OK button of the Clean Paste Behaviour window is pressed
234 Ext
.each(fields
, function (field
) {
235 if (this.dialog
.find('itemId', field
)[0].getValue()) {
236 this.currentBehaviour
= field
;
244 * Handler for paste event
246 * @param object event: the paste event
248 * @return boolean false, if the event was handled, true otherwise
250 onPaste: function (event
) {
251 if (!this.getButton('PasteToggle').inactive
) {
252 switch (this.currentBehaviour
) {
254 // Only IE and WebKit will allow access to the clipboard content, in plain text only however
255 if (Ext
.isIE
|| Ext
.isWebKit
) {
256 var clipboardText
= this.grabClipboardText(event
);
258 this.editor
.getSelection().insertHtml(clipboardText
);
260 return !this.clipboardText
;
262 case 'pasteStructure':
265 // Save the current selection
266 this.bookmark
= this.editor
.getBookMark().get(this.editor
.getSelection().createRange());
267 // Show the pasting pad
270 this.currentBehaviour
,
271 this.getWindowDimensions(
278 event
.browserEvent
.returnValue
= false;
281 // Redirect the paste operation to a hidden section
282 this.redirectPaste();
283 // Process the content of the hidden section after the paste operation is completed
284 // WebKit seems to be pondering a very long time over what is happenning here...
285 this.processPastedContent
.defer(Ext
.isWebKit
? 500 : 50, this);
295 * Grab the text content directly from the clipboard
296 * If successful, stop the paste event
298 * @param object event: the paste event
300 * @return string clipboard content, in plain text, if access was granted
302 grabClipboardText: function (event
) {
303 var clipboardText
= '';
304 // Grab the text content
305 if (window
.clipboardData
|| event
.browserEvent
.clipboardData
|| event
.browserEvent
.dataTransfer
) {
306 clipboardText
= (window
.clipboardData
|| event
.browserEvent
.clipboardData
|| event
.browserEvent
.dataTransfer
).getData('text');
312 // If the user denied access to the clipboard, let the browser paste without intervention
313 TYPO3
.Dialog
.InformationDialog({
314 title
: this.localize('Paste-as-Plain-Text'),
315 msg
: this.localize('Access-to-clipboard-denied')
318 return clipboardText
;
321 * Redirect the paste operation towards a hidden section
325 redirectPaste: function () {
326 // Save the current selection
327 this.bookmark
= this.editor
.getBookMark().get(this.editor
.getSelection().createRange());
328 // Create and append hidden section
329 var hiddenSection
= this.editor
.document
.createElement('div');
330 HTMLArea
.DOM
.addClass(hiddenSection
, 'htmlarea-paste-hidden-section');
331 hiddenSection
.setAttribute('style', 'position: absolute; left: -10000px; top: ' + this.editor
.document
.body
.scrollTop
+ 'px; overflow: hidden;');
332 hiddenSection
= this.editor
.document
.body
.appendChild(hiddenSection
);
334 hiddenSection
.innerHTML
= ' ';
336 // Move the selection to the hidden section and let the browser paste into the hidden section
337 this.editor
.getSelection().selectNodeContents(hiddenSection
);
340 * Process the pasted content that was redirected towards a hidden section
341 * and insert it at the original selection
345 processPastedContent: function () {
347 // Get the hidden section
348 var divs
= this.editor
.document
.getElementsByClassName('htmlarea-paste-hidden-section');
349 var hiddenSection
= divs
[0];
350 // Delete any other hidden sections
351 for (var i
= divs
.length
; --i
>= 1;) {
352 HTMLArea
.DOM
.removeFromParent(divs
[i
]);
355 switch (this.currentBehaviour
) {
357 // Get plain text content
358 content
= hiddenSection
.textContent
;
360 case 'pasteStructure':
363 content
= this.cleaners
[this.currentBehaviour
].render(hiddenSection
, false);
366 // Remove the hidden section from the document
367 HTMLArea
.DOM
.removeFromParent(hiddenSection
);
368 // Restore the selection
369 this.editor
.getSelection().selectRange(this.editor
.getBookMark().moveTo(this.bookmark
));
370 // Insert the cleaned content
372 this.editor
.getSelection().execCommand('insertHTML', false, content
);
376 * Open the pasting pad window (for IE)
378 * @param string buttonId: the button id
379 * @param string title: the window title
380 * @param object dimensions: the opening dimensions of the window
384 openPastingPad: function (buttonId
, title
, dimensions
) {
385 this.dialog
= new Ext
.Window({
386 title
: this.getHelpTip(title
, title
),
387 cls
: 'htmlarea-window',
388 bodyCssClass
: 'pasting-pad',
390 width
: dimensions
.width
,
392 iconCls
: this.getButton(buttonId
).iconCls
,
395 // The document will not be immediately ready
396 fn: function (event
) { this.onPastingPadAfterRender
.defer(100, this, [event
]); },
406 text
: this.getHelpTip('pasteInPastingPad', 'pasteInPastingPad'),
413 itemId
: 'pasting-pad-iframe',
415 name
: 'contentframe',
418 src
: Ext
.isGecko
? 'javascript:void(0);' : HTMLArea
.editorUrl
+ 'popups/blank.html'
423 this.buildButtonConfig('OK', this.onPastingPadOK
),
424 this.buildButtonConfig('Cancel', this.onCancel
)
430 * Handler invoked after the pasting pad iframe has been rendered
432 onPastingPadAfterRender: function () {
433 var iframe
= this.dialog
.getComponent('pasting-pad-iframe').getEl().dom
;
434 var pastingPadDocument
= iframe
.contentWindow
? iframe
.contentWindow
.document
: iframe
.contentDocument
;
435 this.pastingPadBody
= pastingPadDocument
.body
;
436 this.pastingPadBody
.contentEditable
= true;
437 // Start monitoring paste events
438 this.dialog
.mon(Ext
.get(this.pastingPadBody
), 'paste', this.onPastingPadPaste
, this);
439 this.pastingPadBody
.focus();
442 * Handler invoked when content is pasted into the pasting pad
444 onPastingPadPaste: function (event
) {
445 // Let the paste operation complete before cleaning
446 this.cleanPastingPadContents
.defer(50, this);
449 * Clean the contents of the pasting pad
451 cleanPastingPadContents: function () {
452 this.pastingPadBody
.innerHTML
= this.cleaners
[this.currentBehaviour
].render(this.pastingPadBody
, false);
453 this.pastingPadBody
.focus();
456 * Handler invoked when the OK button of the Pasting Pad window is pressed
458 onPastingPadOK: function () {
459 // Restore the selection
460 this.restoreSelection();
461 // Insert the cleaned pasting pad content
462 this.editor
.getSelection().insertHtml(this.pastingPadBody
.innerHTML
);
467 * This function gets called when the toolbar is updated
469 onUpdateToolbar: function (button
, mode
, selectionEmpty
, ancestors
) {
470 if (mode
=== 'wysiwyg' && this.editor
.isEditable()) {
471 switch (button
.itemId
) {
474 title
: this.localize((button
.inactive
? 'enable' : 'disable') + this.currentBehaviour
)