60b57e6de01d92d2b65a258bfa2404321c30f657
[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/HTMLArea/Plugin/Plugin',
18 'TYPO3/CMS/Rtehtmlarea/HTMLArea/UserAgent/UserAgent',
19 'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM',
20 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Event/Event',
21 'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/Walker',
22 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/Util'],
23 function (Plugin, UserAgent, Dom, Event, Walker, Util) {
24
25 var PlainText = function (editor, pluginName) {
26 this.constructor.super.call(this, editor, pluginName);
27 };
28 Util.inherit(PlainText, Plugin);
29 Util.apply(PlainText.prototype, {
30
31 /**
32 * This function gets called by the class constructor
33 */
34 configurePlugin: function (editor) {
35 this.buttonsConfiguration = this.editorConfiguration.buttons;
36
37 /**
38 * Registering plugin "About" information
39 */
40 var pluginInformation = {
41 version : '1.3',
42 developer : 'Stanislas Rolland',
43 developerUrl : 'http://www.sjbr.ca/',
44 copyrightOwner : 'Stanislas Rolland',
45 sponsor : 'Otto van Bruggen',
46 sponsorUrl : 'http://www.webspinnerij.nl',
47 license : 'GPL'
48 };
49 this.registerPluginInformation(pluginInformation);
50
51 /**
52 * Registering the buttons
53 */
54 for (var buttonId in this.buttonList) {
55 var buttonConf = this.buttonList[buttonId];
56 var buttonConfiguration = {
57 id : buttonId,
58 tooltip : this.localize(buttonId + 'Tooltip'),
59 iconCls : 'htmlarea-action-' + buttonConf[1],
60 action : 'onButtonPress',
61 dialog : buttonConf[2]
62 };
63 if (buttonId == 'PasteToggle' && this.buttonsConfiguration && this.buttonsConfiguration[buttonConf[0]] && this.buttonsConfiguration[buttonConf[0]].hidden) {
64 buttonConfiguration.hide = true;
65 buttonConfiguration.hideInContextMenu = true;
66 buttonConfiguration.hotKey = null;
67 this.buttonsConfiguration[buttonConf[0]].hotKey = null;
68 }
69 this.registerButton(buttonConfiguration);
70 }
71 return true;
72 },
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 /**
83 * Cleaner configurations
84 */
85 cleanerConfig: {
86 pasteStructure: {
87 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,
88 removeAttributes: /^(id|on.*|style|class|className|lang|align|valign|bgcolor|color|border|face|.*:.*)$/i
89 },
90 pasteFormat: {
91 keepTags: /^(a|p|h[0-6]|pre|address|article|aside|blockquote|div|footer|header|nav|section|hr|br|img|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,
92 removeAttributes: /^(id|on.*|style|class|className|lang|align|valign|bgcolor|color|border|face|.*:.*)$/i
93 }
94 },
95
96 /**
97 * This function gets called when the plugin is generated
98 */
99 onGenerate: function () {
100 // Create cleaners
101 if (this.buttonsConfiguration && this.buttonsConfiguration['pastebehaviour']) {
102 this.pasteBehaviourConfiguration = this.buttonsConfiguration['pastebehaviour'];
103 }
104 this.cleaners = {};
105 for (var behaviour in this.cleanerConfig) {
106 if (this.pasteBehaviourConfiguration && this.pasteBehaviourConfiguration[behaviour]) {
107 if (this.pasteBehaviourConfiguration[behaviour].keepTags) {
108 this.cleanerConfig[behaviour].keepTags = new RegExp( '^(' + this.pasteBehaviourConfiguration[behaviour].keepTags.split(',').join('|') + ')$', 'i');
109 }
110 if (this.pasteBehaviourConfiguration[behaviour].removeAttributes) {
111 this.cleanerConfig[behaviour].removeAttributes = new RegExp( '^(' + this.pasteBehaviourConfiguration[behaviour].removeAttributes.split(',').join('|') + ')$', 'i');
112 }
113 }
114 this.cleaners[behaviour] = new Walker(this.cleanerConfig[behaviour]);
115 }
116 // Initial behaviour
117 this.currentBehaviour = 'plainText';
118 // May be set in TYPO3 User Settings
119 if (this.buttonsConfiguration && this.buttonsConfiguration['pastebehaviour'] && this.buttonsConfiguration['pastebehaviour']['current']) {
120 this.currentBehaviour = this.buttonsConfiguration['pastebehaviour']['current'];
121 }
122 // Set the toggle ON, if configured
123 if (this.buttonsConfiguration && this.buttonsConfiguration['pastetoggle'] && this.buttonsConfiguration['pastetoggle'].setActiveOnRteOpen) {
124 this.toggleButton('PasteToggle');
125 }
126 // Start monitoring paste events
127 var self = this;
128 Event.on(UserAgent.isIE ? this.editor.document.body : this.editor.document.documentElement, 'paste', function (event) { return self.onPaste(event); });
129 },
130
131 /**
132 * This function toggles the state of a button
133 *
134 * @param {String} buttonId: id of button to be toggled
135 */
136 toggleButton: function (buttonId) {
137 // Set new state
138 var button = this.getButton(buttonId);
139 button.setInactive(!button.inactive);
140 },
141
142 /**
143 * This function gets called when a button was pressed.
144 *
145 * @param {Object} editor: the editor instance
146 * @param {String} id: the button id or the key
147 * @return {Boolean} false if action is completed
148 */
149 onButtonPress: function (editor, id) {
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 );
160 break;
161 case 'PasteToggle':
162 this.toggleButton(buttonId);
163 this.editor.focus();
164 break;
165 }
166 return false;
167 },
168
169 /**
170 * Open the dialogue window
171 *
172 * @param {String} buttonId: the button id
173 * @param {String} title: the window title
174 */
175 openDialogue: function (buttonId, title) {
176 this.dialog = Modal.show(this.localize(title), this.generateDialogContent(), Severity.notice, [
177 this.buildButtonConfig('Next', $.proxy(this.onOK, this), true),
178 this.buildButtonConfig('Done', $.proxy(this.onCancel, this), false, Severity.notice)
179 ]);
180 this.dialog.on('modal-dismiss', $.proxy(this.onClose, this));
181 },
182
183 /**
184 * Generates the content for the dialog window
185 *
186 * @returns {Object}
187 */
188 generateDialogContent: function() {
189 var $fieldset = $('<fieldset />', {'class': 'form-section'});
190 $fieldset.append(
191 $('<h4 />', {'class': 'form-section-headline'}).html(this.getHelpTip('behaviour', title)),
192 $('<div />', {'class': 'form-group col-sm-12'}).append(
193 $('<div />', {'class': 'radio'}).append(
194 $('<label />').html(this.getHelpTip('plainText', 'plainText')).prepend(
195 $('<input />', {type: 'radio', name: buttonId, value: 'plainText'})
196 .prop('checked', this.currentBehaviour === 'plainText')
197 )
198 ),
199 $('<div />', {'class': 'radio'}).append(
200 $('<label />').html(this.getHelpTip('pasteStructure', 'pasteStructure')).prepend(
201 $('<input />', {type: 'radio', name: buttonId, value: 'pasteStructure'})
202 .prop('checked', this.currentBehaviour === 'pasteStructure')
203 )
204 ),
205 $('<div />', {'class': 'radio'}).append(
206 $('<label />').html(this.getHelpTip('pasteFormat', 'pasteFormat')).prepend(
207 $('<input />', {type: 'radio', name: buttonId, value: 'pasteFormat'})
208 .prop('checked', this.currentBehaviour === 'pasteFormat')
209 )
210 )
211 )
212 );
213
214 return $fieldset;
215 },
216
217 /**
218 * Handler invoked when the OK button of the Clean Paste Behaviour window is pressed
219 */
220 onOK: function () {
221 var fields = [
222 'plainText',
223 'pasteStructure',
224 'pasteFormat'
225 ], field;
226 for (var i = fields.length; --i >= 0;) {
227 field = fields[i];
228 if (this.dialog.find('[name="' + field + '"]').val()) {
229 this.currentBehaviour = field;
230 break;
231 }
232 }
233 this.close();
234 return false;
235 },
236
237 /**
238 * Handler for paste event
239 *
240 * @param {Event} event: the jQuery paste event
241 * @return {Boolean} false, if the event was handled, true otherwise
242 */
243 onPaste: function (event) {
244 if (!this.getButton('PasteToggle').inactive) {
245 var clipboardText = '';
246 switch (this.currentBehaviour) {
247 case 'plainText':
248 // Let's see if clipboardData can be used for plain text
249 clipboardText = this.grabClipboardText(event, 'plain');
250 if (clipboardText) {
251 // Stop the event
252 Event.stopEvent(event);
253 this.editor.getSelection().insertHtml(clipboardText);
254 return false;
255 }
256 case 'pasteStructure':
257 case 'pasteFormat':
258 // Let's see if clipboardData can be used for html text
259 clipboardText = this.grabClipboardText(event, 'html');
260 if (clipboardText) {
261 // Stop the event
262 Event.stopEvent(event);
263 // Clean content
264 this.processClipboardContent(clipboardText);
265 return false;
266 }
267 // Could be IE or WebKit denying access to the clipboard
268 if (UserAgent.isIE || UserAgent.isWebKit) {
269 // Show the pasting pad
270 this.openPastingPad(
271 'PasteToggle',
272 this.currentBehaviour
273 );
274 Event.stopEvent(event);
275 return false;
276 } else {
277 // Falling back to old ways...
278 // Redirect the paste operation to a hidden section
279 this.redirectPaste();
280 // Process the content of the hidden section after the paste operation is completed
281 var self = this;
282 window.setTimeout(function () {
283 self.processPastedContent();
284 }, 50);
285 }
286 break;
287 default:
288 break;
289 }
290 }
291 return true;
292 },
293
294 /**
295 * Grab the text content directly from the clipboard
296 * If successful, stop the paste event
297 *
298 * @param {Event} event: the jQuery paste event
299 * @param {String} type: type of content to grab 'plain' ot 'html'
300 * @return {String} clipboard content, if access was granted
301 */
302 grabClipboardText: function (event, type) {
303 var clipboardText = '',
304 browserEvent = Event.getBrowserEvent(event),
305 clipboardData = '',
306 contentTypes = '';
307 if (browserEvent && (browserEvent.clipboardData || window.clipboardData) && (browserEvent.clipboardData || window.clipboardData).getData) {
308 clipboardData = (browserEvent.clipboardData || window.clipboardData);
309 contentTypes = clipboardData.types;
310 }
311 if (clipboardData) {
312 switch (type) {
313 case 'plain':
314 if (/text\/plain/.test(contentTypes) || UserAgent.isIE) {
315 clipboardText = clipboardData.getData(UserAgent.isIE ? 'Text' : 'text/plain');
316 }
317 break;
318 case 'html':
319 if (contentTypes && Object.prototype.toString.call(contentTypes) === '[object Array]' && contentTypes.length > 0) {
320 var i = 0, contentType;
321 while (i < contentTypes.length) {
322 contentType = contentTypes[i];
323 if (/text\/plain|text\/html/.test(contentType)) {
324 clipboardText += clipboardData.getData(contentType);
325 }
326 i++;
327 }
328 }
329 break;
330 }
331 }
332 return clipboardText;
333 },
334
335 /**
336 * Redirect the paste operation towards a hidden section
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.createHiddenSection();
343 // Move the selection to the hidden section and let the browser paste into the hidden section
344 this.editor.getSelection().selectNodeContents(hiddenSection);
345 },
346
347 /**
348 * Create an hidden section inside the RTE content
349 *
350 * @return {Object} the hidden section
351 */
352 createHiddenSection: function () {
353 // Create and append hidden section
354 var hiddenSection = this.editor.document.createElement('div');
355 Dom.addClass(hiddenSection, 'htmlarea-paste-hidden-section');
356 hiddenSection.setAttribute('style', 'position: absolute; left: -10000px; top: ' + this.editor.document.body.scrollTop + 'px; overflow: hidden;');
357 hiddenSection = this.editor.document.body.appendChild(hiddenSection);
358 if (UserAgent.isWebKit) {
359 hiddenSection.innerHTML = '&nbsp;';
360 }
361 return hiddenSection;
362 },
363
364 /**
365 * Process the pasted content that was redirected towards a hidden section
366 * and insert it at the original selection
367 */
368 processPastedContent: function () {
369 this.editor.focus();
370 // Get the hidden section
371 var divs = this.editor.document.getElementsByClassName('htmlarea-paste-hidden-section');
372 var hiddenSection = divs[0];
373 // Delete any other hidden sections
374 for (var i = divs.length; --i >= 1;) {
375 Dom.removeFromParent(divs[i]);
376 }
377 var content = '';
378 switch (this.currentBehaviour) {
379 case 'plainText':
380 // Get plain text content
381 content = hiddenSection.textContent;
382 break;
383 case 'pasteStructure':
384 case 'pasteFormat':
385 // Get clean content
386 content = this.cleaners[this.currentBehaviour].render(hiddenSection, false);
387 break;
388 }
389 // Remove the hidden section from the document
390 Dom.removeFromParent(hiddenSection);
391 // Restore the selection
392 this.editor.getSelection().selectRange(this.editor.getBookMark().moveTo(this.bookmark));
393 // Insert the cleaned content
394 if (content) {
395 this.editor.getSelection().execCommand('insertHTML', false, content);
396 }
397 },
398
399 /**
400 * Process the content that was grabbed form the clipboard
401 * and insert it at the original selection
402 *
403 * @param {String} content: html content grabbed form the clipboard
404 */
405 processClipboardContent: function (content) {
406 this.editor.focus();
407 // Create and append hidden section and insert content
408 var hiddenSection = this.createHiddenSection();
409 hiddenSection.innerHTML = content.replace(/(<html>)|(<body>)|(<\/html>)|(<\/body>)/gi, '');
410 // Get clean content
411 var cleanContent = this.cleaners[this.currentBehaviour].render(hiddenSection, false);
412 // Remove the hidden section from the document
413 Dom.removeFromParent(hiddenSection);
414 // Insert the cleaned content
415 if (cleanContent) {
416 this.editor.getSelection().execCommand('insertHTML', false, cleanContent);
417 }
418 },
419
420 /**
421 * Open the pasting pad window (for IE)
422 *
423 * @param {String} buttonId: the button id
424 * @param {String} title: the window title
425 */
426 openPastingPad: function (buttonId, title) {
427 var self = this;
428
429 this.dialog = Modal.show(this.localize(title), this.generatePastingPadContent(), Severity.notice, [
430 this.buildButtonConfig('Next', $.proxy(this.onPastingPadOK, this), true),
431 this.buildButtonConfig('Done', $.proxy(function() {try { this.close(); } catch (e) {}}, this), false, Severity.notice)
432 ]);
433
434 window.setTimeout(function () {
435 self.onPastingPadAfterRender();
436 }, 200);
437 },
438
439 generatePastingPadContent: function () {
440 return $('<iframe />', {
441 id: 'pasting-pad-iframe',
442 name: 'contentframe',
443 'class': 'contentframe',
444 src: UserAgent.isGecko ? 'javascript:void(0);' : HTMLArea.editorUrl + 'Resources/Public/Html/blank.html'
445 });
446 },
447
448 /**
449 * Handler invoked after the pasting pad iframe has been rendered
450 */
451 onPastingPadAfterRender: function () {
452 var iframe = this.dialog.find('#pasting-pad-iframe').get(0);
453 this.pastingPadDocument = iframe.contentWindow ? iframe.contentWindow.document : iframe.contentDocument;
454 this.pastingPadBody = this.pastingPadDocument.body;
455 this.pastingPadBody.contentEditable = true;
456 var self = this;
457 // Start monitoring paste events
458 Event.on(this.pastingPadBody, 'paste', function (event) {
459 return self.onPastingPadPaste();
460 });
461 // Try to keep focus on the pasting pad
462 Event.on(UserAgent.isIE
463 ? this.editor.document.body
464 : this.editor.document.documentElement, 'mouseover', function (event) {
465 return self.focusOnPastingPad();
466 }
467 );
468 Event.on(this.editor.document.body, 'focus', function (event) {
469 return self.focusOnPastingPad();
470 });
471 Event.on(UserAgent.isIE
472 ? this.pastingPadBody
473 : this.pastingPadDocument.documentElement, 'mouseover', function (event) {
474 return self.focusOnPastingPad();
475 }
476 );
477 this.focusOnPastingPad();
478 },
479
480 /**
481 * Bring focus and selection on the pasting pad
482 */
483 focusOnPastingPad: function () {
484 this.pastingPadBody.focus();
485 if (!UserAgent.isIE) {
486 this.pastingPadDocument.getSelection().selectAllChildren(this.pastingPadBody);
487 }
488 this.pastingPadDocument.getSelection().collapseToEnd();
489 return false;
490 },
491
492 /**
493 * Handler invoked when content is pasted into the pasting pad
494 */
495 onPastingPadPaste: function (event) {
496 // Let the paste operation complete before cleaning
497 var self = this;
498 window.setTimeout(function () {
499 self.cleanPastingPadContents();
500 }, 50);
501 return true;
502 },
503
504 /**
505 * Clean the contents of the pasting pad
506 */
507 cleanPastingPadContents: function () {
508 var content = '';
509 switch (this.currentBehaviour) {
510 case 'plainText':
511 // Get plain text content
512 content = this.pastingPadBody.textContent;
513 break;
514 case 'pasteStructure':
515 case 'pasteFormat':
516 // Get clean content
517 content = this.cleaners[this.currentBehaviour].render(this.pastingPadBody, false);
518 break;
519 }
520 this.pastingPadBody.innerHTML = content;
521 this.pastingPadBody.focus();
522 },
523
524 /**
525 * Handler invoked when the OK button of the Pasting Pad window is pressed
526 */
527 onPastingPadOK: function () {
528 // Restore the selection
529 this.restoreSelection();
530 // Insert the cleaned pasting pad content
531 this.editor.getSelection().insertHtml(this.pastingPadBody.innerHTML);
532 try { this.close(); } catch (e) {}
533 return false;
534 },
535
536 /**
537 * Remove the listeners on the pasing pad
538 */
539 removeListeners: function () {
540 if (this.pastingPadBody) {
541 Event.off(this.pastingPadBody);
542 Event.off(this.pastingPadDocument.documentElement);
543 }
544 },
545
546 /**
547 * This function gets called when the toolbar is updated
548 */
549 onUpdateToolbar: function (button, mode, selectionEmpty, ancestors) {
550 if (mode === 'wysiwyg' && this.editor.isEditable()) {
551 switch (button.itemId) {
552 case 'PasteToggle':
553 button.setTooltip(this.localize((button.inactive ? 'enable' : 'disable') + this.currentBehaviour));
554 break;
555 }
556 }
557 }
558 });
559
560 return PlainText;
561 });