bec8a32ec05310778d10a8c271b720b16508e76f
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / htmlarea / plugins / PlainText / plain-text.js
1 /***************************************************************
2 * Copyright notice
3 *
4 * (c) 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 * Paste as Plain Text Plugin for TYPO3 htmlArea RTE
29 */
30 HTMLArea.PlainText = Ext.extend(HTMLArea.Plugin, {
31 /*
32 * This function gets called by the class constructor
33 */
34 configurePlugin: function (editor) {
35 this.buttonsConfiguration = this.editorConfiguration.buttons;
36 /*
37 * Registering plugin "About" information
38 */
39 var pluginInformation = {
40 version : '1.3',
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',
46 license : 'GPL'
47 };
48 this.registerPluginInformation(pluginInformation);
49 /*
50 * Registering the buttons
51 */
52 Ext.iterate(this.buttonList, function (buttonId, buttonConf) {
53 var buttonConfiguration = {
54 id : buttonId,
55 tooltip : this.localize(buttonId + 'Tooltip'),
56 iconCls : 'htmlarea-action-' + buttonConf[1],
57 action : 'onButtonPress',
58 dialog : buttonConf[2]
59 };
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;
65 }
66 this.registerButton(buttonConfiguration);
67 }, this);
68 return true;
69 },
70 /*
71 * The list of buttons added by this plugin
72 */
73 buttonList: {
74 PasteToggle: ['pastetoggle', 'paste-toggle', false],
75 PasteBehaviour: ['pastebehaviour', 'paste-behaviour', true]
76 },
77 /*
78 * Cleaner configurations
79 */
80 cleanerConfig: {
81 pasteStructure: {
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
84 },
85 pasteFormat: {
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
88 }
89 },
90 /*
91 * This function gets called when the plugin is generated
92 */
93 onGenerate: function () {
94 // Create cleaners
95 if (this.buttonsConfiguration && this.buttonsConfiguration['pastebehaviour']) {
96 this.pasteBehaviourConfiguration = this.buttonsConfiguration['pastebehaviour'];
97 }
98 this.cleaners = {};
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');
103 }
104 if (this.pasteBehaviourConfiguration[behaviour].removeAttributes) {
105 this.cleanerConfig[behaviour].removeAttributes = new RegExp( '^(' + this.pasteBehaviourConfiguration[behaviour].removeAttributes.split(',').join('|') + ')$', 'i');
106 }
107 }
108 this.cleaners[behaviour] = new HTMLArea.DOM.Walker(this.cleanerConfig[behaviour]);
109 }, this);
110 // Initial 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'];
115 }
116 // Set the toggle ON, if configured
117 if (this.buttonsConfiguration && this.buttonsConfiguration['pastetoggle'] && this.buttonsConfiguration['pastetoggle'].setActiveOnRteOpen) {
118 this.toggleButton('PasteToggle');
119 }
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);
122 },
123 /*
124 * This function toggles the state of a button
125 *
126 * @param string buttonId: id of button to be toggled
127 *
128 * @return void
129 */
130 toggleButton: function (buttonId) {
131 // Set new state
132 var button = this.getButton(buttonId);
133 button.setInactive(!button.inactive);
134 },
135 /*
136 * This function gets called when a button was pressed.
137 *
138 * @param object editor: the editor instance
139 * @param string id: the button id or the key
140 *
141 * @return boolean false if action is completed
142 */
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;
147 switch (buttonId) {
148 case 'PasteBehaviour':
149 // Open dialogue window
150 this.openDialogue(
151 buttonId,
152 'PasteBehaviourTooltip',
153 this.getWindowDimensions(
154 {
155 width: 260,
156 height:260
157 },
158 buttonId
159 )
160 );
161 break;
162 case 'PasteToggle':
163 this.toggleButton(buttonId);
164 this.editor.focus();
165 break;
166 }
167 return false;
168 },
169 /*
170 * Open the dialogue window
171 *
172 * @param string buttonId: the button id
173 * @param string title: the window title
174 * @param object dimensions: the opening dimensions of the window
175 *
176 * @return void
177 */
178 openDialogue: function (buttonId, title, dimensions) {
179 this.dialog = new Ext.Window({
180 title: this.localize(title),
181 cls: 'htmlarea-window',
182 border: false,
183 width: dimensions.width,
184 height: 'auto',
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,
188 listeners: {
189 close: {
190 fn: this.onClose,
191 scope: this
192 }
193 },
194 items: [{
195 xtype: 'fieldset',
196 defaultType: 'radio',
197 title: this.getHelpTip('behaviour', title),
198 labelWidth: 170,
199 defaults: {
200 labelSeparator: '',
201 name: buttonId
202 },
203 items: [{
204 itemId: 'plainText',
205 fieldLabel: this.getHelpTip('plainText', 'plainText'),
206 checked: (this.currentBehaviour === 'plainText')
207 },{
208 itemId: 'pasteStructure',
209 fieldLabel: this.getHelpTip('pasteStructure', 'pasteStructure'),
210 checked: (this.currentBehaviour === 'pasteStructure')
211 },{
212 itemId: 'pasteFormat',
213 fieldLabel: this.getHelpTip('pasteFormat', 'pasteFormat'),
214 checked: (this.currentBehaviour === 'pasteFormat')
215 }
216 ]
217 }
218 ],
219 buttons: [
220 this.buildButtonConfig('OK', this.onOK)
221 ]
222 });
223 this.show();
224 },
225 /*
226 * Handler invoked when the OK button of the Clean Paste Behaviour window is pressed
227 */
228 onOK: function () {
229 var fields = [
230 'plainText',
231 'pasteStructure',
232 'pasteFormat'
233 ];
234 Ext.each(fields, function (field) {
235 if (this.dialog.find('itemId', field)[0].getValue()) {
236 this.currentBehaviour = field;
237 return false;
238 }
239 }, this);
240 this.close();
241 return false;
242 },
243 /*
244 * Handler for paste event
245 *
246 * @param object event: the paste event
247 *
248 * @return boolean false, if the event was handled, true otherwise
249 */
250 onPaste: function (event) {
251 if (!this.getButton('PasteToggle').inactive) {
252 switch (this.currentBehaviour) {
253 case 'plainText':
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);
257 if (clipboardText) {
258 this.editor.getSelection().insertHtml(clipboardText);
259 }
260 return !this.clipboardText;
261 }
262 case 'pasteStructure':
263 case 'pasteFormat':
264 if (Ext.isIE) {
265 // Save the current selection
266 this.bookmark = this.editor.getBookMark().get(this.editor.getSelection().createRange());
267 // Show the pasting pad
268 this.openPastingPad(
269 'PasteToggle',
270 this.currentBehaviour,
271 this.getWindowDimensions(
272 {
273 width: 550,
274 height: 550
275 },
276 'PasteToggle'
277 ));
278 event.browserEvent.returnValue = false;
279 return false;
280 } else {
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);
286 }
287 break;
288 default:
289 break;
290 }
291 }
292 return true;
293 },
294 /*
295 * Grab the text content directly from the clipboard
296 * If successful, stop the paste event
297 *
298 * @param object event: the paste event
299 *
300 * @return string clipboard content, in plain text, if access was granted
301 */
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');
307 }
308 if (clipboardText) {
309 // Stop the event
310 event.stopEvent();
311 } else {
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')
316 });
317 }
318 return clipboardText;
319 },
320 /*
321 * Redirect the paste operation towards a hidden section
322 *
323 * @return void
324 */
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);
333 if (Ext.isWebKit) {
334 hiddenSection.innerHTML = '&nbsp;';
335 }
336 // Move the selection to the hidden section and let the browser paste into the hidden section
337 this.editor.getSelection().selectNodeContents(hiddenSection);
338 },
339 /*
340 * Process the pasted content that was redirected towards a hidden section
341 * and insert it at the original selection
342 *
343 * @return void
344 */
345 processPastedContent: function () {
346 this.editor.focus();
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]);
353 }
354 var content = '';
355 switch (this.currentBehaviour) {
356 case 'plainText':
357 // Get plain text content
358 content = hiddenSection.textContent;
359 break;
360 case 'pasteStructure':
361 case 'pasteFormat':
362 // Get clean content
363 content = this.cleaners[this.currentBehaviour].render(hiddenSection, false);
364 break;
365 }
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
371 if (content) {
372 this.editor.getSelection().execCommand('insertHTML', false, content);
373 }
374 },
375 /*
376 * Open the pasting pad window (for IE)
377 *
378 * @param string buttonId: the button id
379 * @param string title: the window title
380 * @param object dimensions: the opening dimensions of the window
381 *
382 * @return void
383 */
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',
389 border: false,
390 width: dimensions.width,
391 height: 'auto',
392 iconCls: this.getButton(buttonId).iconCls,
393 listeners: {
394 afterrender: {
395 // The document will not be immediately ready
396 fn: function (event) { this.onPastingPadAfterRender.defer(100, this, [event]); },
397 scope: this
398 },
399 close: {
400 fn: this.onClose,
401 scope: this
402 }
403 },
404 items: [{
405 xtype: 'tbtext',
406 text: this.getHelpTip('pasteInPastingPad', 'pasteInPastingPad'),
407 style: {
408 marginBottom: '5px'
409 }
410 },{
411 // The iframe
412 xtype: 'box',
413 itemId: 'pasting-pad-iframe',
414 autoEl: {
415 name: 'contentframe',
416 tag: 'iframe',
417 cls: 'contentframe',
418 src: Ext.isGecko ? 'javascript:void(0);' : HTMLArea.editorUrl + 'popups/blank.html'
419 }
420 }
421 ],
422 buttons: [
423 this.buildButtonConfig('OK', this.onPastingPadOK),
424 this.buildButtonConfig('Cancel', this.onCancel)
425 ]
426 });
427 this.show();
428 },
429 /*
430 * Handler invoked after the pasting pad iframe has been rendered
431 */
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();
440 },
441 /*
442 * Handler invoked when content is pasted into the pasting pad
443 */
444 onPastingPadPaste: function (event) {
445 // Let the paste operation complete before cleaning
446 this.cleanPastingPadContents.defer(50, this);
447 },
448 /*
449 * Clean the contents of the pasting pad
450 */
451 cleanPastingPadContents: function () {
452 this.pastingPadBody.innerHTML = this.cleaners[this.currentBehaviour].render(this.pastingPadBody, false);
453 this.pastingPadBody.focus();
454 },
455 /*
456 * Handler invoked when the OK button of the Pasting Pad window is pressed
457 */
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);
463 this.close();
464 return false;
465 },
466 /*
467 * This function gets called when the toolbar is updated
468 */
469 onUpdateToolbar: function (button, mode, selectionEmpty, ancestors) {
470 if (mode === 'wysiwyg' && this.editor.isEditable()) {
471 switch (button.itemId) {
472 case 'PasteToggle':
473 button.setTooltip({
474 title: this.localize((button.inactive ? 'enable' : 'disable') + this.currentBehaviour)
475 });
476 break;
477 }
478 }
479 }
480 });