660570b98b6fd9cad628b1dbde3047a915de2585
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / Resources / Public / JavaScript / Plugins / PlainText.js
1 /*
2 * This file is part of the TYPO3 CMS project.
3 *
4 * It is free software; you can redistribute it and/or modify it under
5 * the terms of the GNU General Public License, either version 2
6 * of the License, or any later version.
7 *
8 * For the full copyright and license information, please read the
9 * LICENSE.txt file that was distributed with this source code.
10 *
11 * The TYPO3 project - inspiring people to share!
12 */
13
14 /**
15 * Paste as Plain Text Plugin for TYPO3 htmlArea RTE
16 */
17 define('TYPO3/CMS/Rtehtmlarea/Plugins/PlainText',
18 ['TYPO3/CMS/Rtehtmlarea/HTMLArea/Plugin/Plugin',
19 'TYPO3/CMS/Rtehtmlarea/HTMLArea/UserAgent/UserAgent',
20 'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM',
21 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Event/Event',
22 'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/Walker',
23 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/Util'],
24 function (Plugin, UserAgent, Dom, Event, Walker, Util) {
25
26 var PlainText = function (editor, pluginName) {
27 this.constructor.super.call(this, editor, pluginName);
28 };
29 Util.inherit(PlainText, Plugin);
30 Util.apply(PlainText.prototype, {
31
32 /**
33 * This function gets called by the class constructor
34 */
35 configurePlugin: function (editor) {
36 this.buttonsConfiguration = this.editorConfiguration.buttons;
37
38 /**
39 * Registering plugin "About" information
40 */
41 var pluginInformation = {
42 version : '1.3',
43 developer : 'Stanislas Rolland',
44 developerUrl : 'http://www.sjbr.ca/',
45 copyrightOwner : 'Stanislas Rolland',
46 sponsor : 'Otto van Bruggen',
47 sponsorUrl : 'http://www.webspinnerij.nl',
48 license : 'GPL'
49 };
50 this.registerPluginInformation(pluginInformation);
51
52 /**
53 * Registering the buttons
54 */
55 for (var buttonId in this.buttonList) {
56 var buttonConf = this.buttonList[buttonId];
57 var buttonConfiguration = {
58 id : buttonId,
59 tooltip : this.localize(buttonId + 'Tooltip'),
60 iconCls : 'htmlarea-action-' + buttonConf[1],
61 action : 'onButtonPress',
62 dialog : buttonConf[2]
63 };
64 if (buttonId == 'PasteToggle' && this.buttonsConfiguration && this.buttonsConfiguration[buttonConf[0]] && this.buttonsConfiguration[buttonConf[0]].hidden) {
65 buttonConfiguration.hide = true;
66 buttonConfiguration.hideInContextMenu = true;
67 buttonConfiguration.hotKey = null;
68 this.buttonsConfiguration[buttonConf[0]].hotKey = null;
69 }
70 this.registerButton(buttonConfiguration);
71 }
72 return true;
73 },
74 /*
75 * The list of buttons added by this plugin
76 */
77 buttonList: {
78 PasteToggle: ['pastetoggle', 'paste-toggle', false],
79 PasteBehaviour: ['pastebehaviour', 'paste-behaviour', true]
80 },
81 /*
82 * Cleaner configurations
83 */
84 cleanerConfig: {
85 pasteStructure: {
86 keepTags: /^(a|p|h[0-6]|pre|address|article|aside|blockquote|div|footer|header|nav|section|hr|br|table|thead|tbody|tfoot|caption|tr|th|td|ul|ol|dl|li|dt|dd)$/i,
87 removeAttributes: /^(id|on*|style|class|className|lang|align|valign|bgcolor|color|border|face|.*:.*)$/i
88 },
89 pasteFormat: {
90 keepTags: /^(a|p|h[0-6]|pre|address|article|aside|blockquote|div|footer|header|nav|section|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,
91 removeAttributes: /^(id|on*|style|class|className|lang|align|valign|bgcolor|color|border|face|.*:.*)$/i
92 }
93 },
94 /*
95 * This function gets called when the plugin is generated
96 */
97 onGenerate: function () {
98 // Create cleaners
99 if (this.buttonsConfiguration && this.buttonsConfiguration['pastebehaviour']) {
100 this.pasteBehaviourConfiguration = this.buttonsConfiguration['pastebehaviour'];
101 }
102 this.cleaners = {};
103 for (var behaviour in this.cleanerConfig) {
104 if (this.pasteBehaviourConfiguration && this.pasteBehaviourConfiguration[behaviour]) {
105 if (this.pasteBehaviourConfiguration[behaviour].keepTags) {
106 this.cleanerConfig[behaviour].keepTags = new RegExp( '^(' + this.pasteBehaviourConfiguration[behaviour].keepTags.split(',').join('|') + ')$', 'i');
107 }
108 if (this.pasteBehaviourConfiguration[behaviour].removeAttributes) {
109 this.cleanerConfig[behaviour].removeAttributes = new RegExp( '^(' + this.pasteBehaviourConfiguration[behaviour].removeAttributes.split(',').join('|') + ')$', 'i');
110 }
111 }
112 this.cleaners[behaviour] = new Walker(this.cleanerConfig[behaviour]);
113 }
114 // Initial behaviour
115 this.currentBehaviour = 'plainText';
116 // May be set in TYPO3 User Settings
117 if (this.buttonsConfiguration && this.buttonsConfiguration['pastebehaviour'] && this.buttonsConfiguration['pastebehaviour']['current']) {
118 this.currentBehaviour = this.buttonsConfiguration['pastebehaviour']['current'];
119 }
120 // Set the toggle ON, if configured
121 if (this.buttonsConfiguration && this.buttonsConfiguration['pastetoggle'] && this.buttonsConfiguration['pastetoggle'].setActiveOnRteOpen) {
122 this.toggleButton('PasteToggle');
123 }
124 // Start monitoring paste events
125 var self = this;
126 Event.on(UserAgent.isIE ? this.editor.document.body : this.editor.document.documentElement, 'paste', function (event) { return self.onPaste(event); });
127 },
128
129 /**
130 * This function toggles the state of a button
131 *
132 * @param string buttonId: id of button to be toggled
133 *
134 * @return void
135 */
136 toggleButton: function (buttonId) {
137 // Set new state
138 var button = this.getButton(buttonId);
139 button.setInactive(!button.inactive);
140 },
141 /*
142 * This function gets called when a button was pressed.
143 *
144 * @param object editor: the editor instance
145 * @param string id: the button id or the key
146 *
147 * @return boolean false if action is completed
148 */
149 onButtonPress: function (editor, id, target) {
150 // Could be a button or its hotkey
151 var buttonId = this.translateHotKey(id);
152 buttonId = buttonId ? buttonId : id;
153 switch (buttonId) {
154 case 'PasteBehaviour':
155 // Open dialogue window
156 this.openDialogue(
157 buttonId,
158 'PasteBehaviourTooltip',
159 this.getWindowDimensions(
160 {
161 width: 260,
162 height:260
163 },
164 buttonId
165 )
166 );
167 break;
168 case 'PasteToggle':
169 this.toggleButton(buttonId);
170 this.editor.focus();
171 break;
172 }
173 return false;
174 },
175 /*
176 * Open the dialogue window
177 *
178 * @param string buttonId: the button id
179 * @param string title: the window title
180 * @param object dimensions: the opening dimensions of the window
181 *
182 * @return void
183 */
184 openDialogue: function (buttonId, title, dimensions) {
185 this.dialog = new Ext.Window({
186 title: this.localize(title),
187 cls: 'htmlarea-window',
188 border: false,
189 width: dimensions.width,
190 height: 'auto',
191 iconCls: this.getButton(buttonId).iconCls,
192 listeners: {
193 close: {
194 fn: this.onClose,
195 scope: this
196 }
197 },
198 items: [{
199 xtype: 'fieldset',
200 defaultType: 'radio',
201 title: this.getHelpTip('behaviour', title),
202 labelWidth: 170,
203 defaults: {
204 labelSeparator: '',
205 name: buttonId
206 },
207 items: [{
208 itemId: 'plainText',
209 fieldLabel: this.getHelpTip('plainText', 'plainText'),
210 checked: (this.currentBehaviour === 'plainText')
211 },{
212 itemId: 'pasteStructure',
213 fieldLabel: this.getHelpTip('pasteStructure', 'pasteStructure'),
214 checked: (this.currentBehaviour === 'pasteStructure')
215 },{
216 itemId: 'pasteFormat',
217 fieldLabel: this.getHelpTip('pasteFormat', 'pasteFormat'),
218 checked: (this.currentBehaviour === 'pasteFormat')
219 }
220 ]
221 }
222 ],
223 buttons: [
224 this.buildButtonConfig('OK', this.onOK)
225 ]
226 });
227 this.show();
228 },
229 /*
230 * Handler invoked when the OK button of the Clean Paste Behaviour window is pressed
231 */
232 onOK: function () {
233 var fields = [
234 'plainText',
235 'pasteStructure',
236 'pasteFormat'
237 ], field;
238 for (var i = fields.length; --i >= 0;) {
239 field = fields[i];
240 if (this.dialog.find('itemId', field)[0].getValue()) {
241 this.currentBehaviour = field;
242 break;
243 }
244 }
245 this.close();
246 return false;
247 },
248
249 /**
250 * Handler for paste event
251 *
252 * @param object event: the jQuery paste event
253 * @return boolean false, if the event was handled, true otherwise
254 */
255 onPaste: function (event) {
256 if (!this.getButton('PasteToggle').inactive) {
257 switch (this.currentBehaviour) {
258 case 'plainText':
259 // Only IE before IE9 and Chrome will allow access to the clipboard content by default, in plain text only however
260 if (UserAgent.isIEBeforeIE9 || UserAgent.isChrome) {
261 var clipboardText = this.grabClipboardText(event);
262 if (clipboardText) {
263 this.editor.getSelection().insertHtml(clipboardText);
264 }
265 return !this.clipboardText;
266 }
267 case 'pasteStructure':
268 case 'pasteFormat':
269 if (UserAgent.isIE) {
270 // Show the pasting pad
271 this.openPastingPad(
272 'PasteToggle',
273 this.currentBehaviour,
274 this.getWindowDimensions(
275 {
276 width: 550,
277 height: 550
278 },
279 'PasteToggle'
280 )
281 );
282 if (UserAgent.isIEBeforeIE9) {
283 Event.getBrowserEvent(event).returnValue = false;
284 } else {
285 Event.stopEvent(event);
286 }
287 return false;
288 } else {
289 // Redirect the paste operation to a hidden section
290 this.redirectPaste();
291 // Process the content of the hidden section after the paste operation is completed
292 // WebKit seems to be pondering a very long time over what is happenning here...
293 var self = this;
294 window.setTimeout(function () {
295 self.processPastedContent();
296 }, UserAgent.isWebKit ? 500 : 50);
297 }
298 break;
299 default:
300 break;
301 }
302 }
303 return true;
304 },
305
306 /**
307 * Grab the text content directly from the clipboard
308 * If successful, stop the paste event
309 *
310 * @param object event: the jQuery paste event
311 * @return string clipboard content, in plain text, if access was granted
312 */
313 grabClipboardText: function (event) {
314 var clipboardText = '';
315 var browserEvent = Event.getBrowserEvent(event);
316 // Grab the text content
317 if (window.clipboardData || browserEvent.clipboardData || browserEvent.dataTransfer) {
318 clipboardText = (window.clipboardData || browserEvent.clipboardData || browserEvent.dataTransfer).getData('text');
319 }
320 if (clipboardText) {
321 // Stop the event
322 Event.stopEvent(event);
323 } else {
324 // If the user denied access to the clipboard, let the browser paste without intervention
325 TYPO3.Dialog.InformationDialog({
326 title: this.localize('Paste-as-Plain-Text'),
327 msg: this.localize('Access-to-clipboard-denied')
328 });
329 }
330 return clipboardText;
331 },
332
333 /**
334 * Redirect the paste operation towards a hidden section
335 *
336 * @return void
337 */
338 redirectPaste: function () {
339 // Save the current selection
340 this.bookmark = this.editor.getBookMark().get(this.editor.getSelection().createRange());
341 // Create and append hidden section
342 var hiddenSection = this.editor.document.createElement('div');
343 Dom.addClass(hiddenSection, 'htmlarea-paste-hidden-section');
344 hiddenSection.setAttribute('style', 'position: absolute; left: -10000px; top: ' + this.editor.document.body.scrollTop + 'px; overflow: hidden;');
345 hiddenSection = this.editor.document.body.appendChild(hiddenSection);
346 if (UserAgent.isWebKit) {
347 hiddenSection.innerHTML = ' ';
348 }
349 // Move the selection to the hidden section and let the browser paste into the hidden section
350 this.editor.getSelection().selectNodeContents(hiddenSection);
351 },
352 /*
353 * Process the pasted content that was redirected towards a hidden section
354 * and insert it at the original selection
355 *
356 * @return void
357 */
358 processPastedContent: function () {
359 this.editor.focus();
360 // Get the hidden section
361 var divs = this.editor.document.getElementsByClassName('htmlarea-paste-hidden-section');
362 var hiddenSection = divs[0];
363 // Delete any other hidden sections
364 for (var i = divs.length; --i >= 1;) {
365 Dom.removeFromParent(divs[i]);
366 }
367 var content = '';
368 switch (this.currentBehaviour) {
369 case 'plainText':
370 // Get plain text content
371 content = hiddenSection.textContent;
372 break;
373 case 'pasteStructure':
374 case 'pasteFormat':
375 // Get clean content
376 content = this.cleaners[this.currentBehaviour].render(hiddenSection, false);
377 break;
378 }
379 // Remove the hidden section from the document
380 Dom.removeFromParent(hiddenSection);
381 // Restore the selection
382 this.editor.getSelection().selectRange(this.editor.getBookMark().moveTo(this.bookmark));
383 // Insert the cleaned content
384 if (content) {
385 this.editor.getSelection().execCommand('insertHTML', false, content);
386 }
387 },
388
389 /**
390 * Open the pasting pad window (for IE)
391 *
392 * @param string buttonId: the button id
393 * @param string title: the window title
394 * @param object dimensions: the opening dimensions of the window
395 * @return void
396 */
397 openPastingPad: function (buttonId, title, dimensions) {
398 var self = this;
399 this.dialog = new Ext.Window({
400 title: this.getHelpTip(title, title),
401 cls: 'htmlarea-window',
402 bodyCssClass: 'pasting-pad',
403 border: false,
404 width: dimensions.width,
405 height: 'auto',
406 iconCls: this.getButton(buttonId).iconCls,
407 listeners: {
408 afterrender: {
409 // The document will not be immediately ready
410 fn: function (event) {
411 window.setTimeout(function () {
412 self.onPastingPadAfterRender();
413 }, 100);
414 }
415 },
416 close: {
417 fn: this.onPastingPadClose,
418 scope: this
419 }
420 },
421 items: [{
422 xtype: 'tbtext',
423 text: this.getHelpTip('pasteInPastingPad', 'pasteInPastingPad'),
424 style: {
425 marginBottom: '5px'
426 }
427 },{
428 // The iframe
429 xtype: 'box',
430 itemId: 'pasting-pad-iframe',
431 autoEl: {
432 name: 'contentframe',
433 tag: 'iframe',
434 cls: 'contentframe',
435 src: UserAgent.isGecko ? 'javascript:void(0);' : HTMLArea.editorUrl + 'Resources/Public/Html/blank.html'
436 }
437 }
438 ],
439 buttons: [
440 this.buildButtonConfig('OK', this.onPastingPadOK),
441 this.buildButtonConfig('Cancel', this.onCancel)
442 ]
443 });
444 // Apparently, IE needs some time before being able to show the iframe
445 window.setTimeout(function () {
446 self.show();
447 }, 100);
448 },
449
450 /**
451 * Handler invoked after the pasting pad iframe has been rendered
452 */
453 onPastingPadAfterRender: function () {
454 var iframe = this.dialog.getComponent('pasting-pad-iframe').getEl().dom;
455 this.pastingPadDocument = iframe.contentWindow ? iframe.contentWindow.document : iframe.contentDocument;
456 this.pastingPadBody = this.pastingPadDocument.body;
457 this.pastingPadBody.contentEditable = true;
458 var self = this;
459 // Start monitoring paste events
460 Event.on(this.pastingPadBody, 'paste', function (event) { return self.onPastingPadPaste(); });
461 // Try to keep focus on the pasting pad
462 Event.on(UserAgent.isIE ? this.editor.document.body : this.editor.document.documentElement, 'mouseover', function (event) { return self.focusOnPastingPad(); });
463 Event.on(this.editor.document.body, 'focus', function (event) { return self.focusOnPastingPad(); });
464 Event.on(UserAgent.isIE ? this.pastingPadBody: this.pastingPadDocument.documentElement, 'mouseover', function (event) { return self.focusOnPastingPad(); });
465 this.focusOnPastingPad();
466 },
467
468 /**
469 * Bring focus and selection on the pasting pad
470 */
471 focusOnPastingPad: function () {
472 this.pastingPadBody.focus();
473 if (!UserAgent.isIE) {
474 this.pastingPadDocument.getSelection().selectAllChildren(this.pastingPadBody);
475 }
476 this.pastingPadDocument.getSelection().collapseToEnd();
477 return false;
478 },
479
480 /**
481 * Handler invoked when content is pasted into the pasting pad
482 */
483 onPastingPadPaste: function (event) {
484 // Let the paste operation complete before cleaning
485 var self = this;
486 window.setTimeout(function () {
487 self.cleanPastingPadContents();
488 }, 50);
489 return true;
490 },
491
492 /**
493 * Clean the contents of the pasting pad
494 */
495 cleanPastingPadContents: function () {
496 var content = '';
497 switch (this.currentBehaviour) {
498 case 'plainText':
499 // Get plain text content
500 content = this.pastingPadBody.textContent;
501 break;
502 case 'pasteStructure':
503 case 'pasteFormat':
504 // Get clean content
505 content = this.cleaners[this.currentBehaviour].render(this.pastingPadBody, false);
506 break;
507 }
508 this.pastingPadBody.innerHTML = content;
509 this.pastingPadBody.focus();
510 },
511
512 /**
513 * Handler invoked when the OK button of the Pasting Pad window is pressed
514 */
515 onPastingPadOK: function () {
516 // Restore the selection
517 this.restoreSelection();
518 // Insert the cleaned pasting pad content
519 this.editor.getSelection().insertHtml(this.pastingPadBody.innerHTML);
520 this.close();
521 return false;
522 },
523
524 /**
525 * Remove the listeners on the pasing pad
526 */
527 removeListeners: function () {
528 if(this.pastingPadBody) {
529 Event.off(this.pastingPadBody);
530 Event.off(this.pastingPadDocument.documentElement);
531 }
532 },
533
534 /**
535 * This function gets called when the toolbar is updated
536 */
537 onUpdateToolbar: function (button, mode, selectionEmpty, ancestors) {
538 if (mode === 'wysiwyg' && this.editor.isEditable()) {
539 switch (button.itemId) {
540 case 'PasteToggle':
541 button.setTooltip(this.localize((button.inactive ? 'enable' : 'disable') + this.currentBehaviour));
542 break;
543 }
544 }
545 }
546 });
547
548 return PlainText;
549
550 });