JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
vanilla ckeditor-3.6.6.1
[ckeditor.git] / _source / plugins / clipboard / plugin.js
1 /*\r
2 Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
3 For licensing, see LICENSE.html or http://ckeditor.com/license\r
4 */\r
5 \r
6 /**\r
7  * @file Clipboard support\r
8  */\r
9 \r
10 (function()\r
11 {\r
12         // Tries to execute any of the paste, cut or copy commands in IE. Returns a\r
13         // boolean indicating that the operation succeeded.\r
14         var execIECommand = function( editor, command )\r
15         {\r
16                 var doc = editor.document,\r
17                         body = doc.getBody();\r
18 \r
19                 var enabled = false;\r
20                 var onExec = function()\r
21                 {\r
22                         enabled = true;\r
23                 };\r
24 \r
25                 // The following seems to be the only reliable way to detect that\r
26                 // clipboard commands are enabled in IE. It will fire the\r
27                 // onpaste/oncut/oncopy events only if the security settings allowed\r
28                 // the command to execute.\r
29                 body.on( command, onExec );\r
30 \r
31                 // IE6/7: document.execCommand has problem to paste into positioned element.\r
32                 ( CKEDITOR.env.version > 7 ? doc.$ : doc.$.selection.createRange() ) [ 'execCommand' ]( command );\r
33 \r
34                 body.removeListener( command, onExec );\r
35 \r
36                 return enabled;\r
37         };\r
38 \r
39         // Attempts to execute the Cut and Copy operations.\r
40         var tryToCutCopy =\r
41                 CKEDITOR.env.ie ?\r
42                         function( editor, type )\r
43                         {\r
44                                 return execIECommand( editor, type );\r
45                         }\r
46                 :               // !IE.\r
47                         function( editor, type )\r
48                         {\r
49                                 try\r
50                                 {\r
51                                         // Other browsers throw an error if the command is disabled.\r
52                                         return editor.document.$.execCommand( type, false, null );\r
53                                 }\r
54                                 catch( e )\r
55                                 {\r
56                                         return false;\r
57                                 }\r
58                         };\r
59 \r
60         // A class that represents one of the cut or copy commands.\r
61         var cutCopyCmd = function( type )\r
62         {\r
63                 this.type = type;\r
64                 this.canUndo = this.type == 'cut';              // We can't undo copy to clipboard.\r
65                 this.startDisabled = true;\r
66         };\r
67 \r
68         cutCopyCmd.prototype =\r
69         {\r
70                 exec : function( editor, data )\r
71                 {\r
72                         this.type == 'cut' && fixCut( editor );\r
73 \r
74                         var success = tryToCutCopy( editor, this.type );\r
75 \r
76                         if ( !success )\r
77                                 alert( editor.lang.clipboard[ this.type + 'Error' ] );          // Show cutError or copyError.\r
78 \r
79                         return success;\r
80                 }\r
81         };\r
82 \r
83         // Paste command.\r
84         var pasteCmd =\r
85         {\r
86                 canUndo : false,\r
87 \r
88                 exec :\r
89                         CKEDITOR.env.ie ?\r
90                                 function( editor )\r
91                                 {\r
92                                         // Prevent IE from pasting at the begining of the document.\r
93                                         editor.focus();\r
94 \r
95                                         if ( !editor.document.getBody().fire( 'beforepaste' )\r
96                                                  && !execIECommand( editor, 'paste' ) )\r
97                                         {\r
98                                                 editor.fire( 'pasteDialog' );\r
99                                                 return false;\r
100                                         }\r
101                                 }\r
102                         :\r
103                                 function( editor )\r
104                                 {\r
105                                         try\r
106                                         {\r
107                                                 if ( !editor.document.getBody().fire( 'beforepaste' )\r
108                                                          && !editor.document.$.execCommand( 'Paste', false, null ) )\r
109                                                 {\r
110                                                         throw 0;\r
111                                                 }\r
112                                         }\r
113                                         catch ( e )\r
114                                         {\r
115                                                 setTimeout( function()\r
116                                                         {\r
117                                                                 editor.fire( 'pasteDialog' );\r
118                                                         }, 0 );\r
119                                                 return false;\r
120                                         }\r
121                                 }\r
122         };\r
123 \r
124         // Listens for some clipboard related keystrokes, so they get customized.\r
125         var onKey = function( event )\r
126         {\r
127                 if ( this.mode != 'wysiwyg' )\r
128                         return;\r
129 \r
130                 switch ( event.data.keyCode )\r
131                 {\r
132                         // Paste\r
133                         case CKEDITOR.CTRL + 86 :               // CTRL+V\r
134                         case CKEDITOR.SHIFT + 45 :              // SHIFT+INS\r
135 \r
136                                 var body = this.document.getBody();\r
137 \r
138                                 // 1. Opera just misses the "paste" event.\r
139                                 // 2. Firefox's "paste" event comes too late to have the plain\r
140                                 // text paste bin to work.\r
141                                 if ( CKEDITOR.env.opera || CKEDITOR.env.gecko )\r
142                                         body.fire( 'paste' );\r
143                                 return;\r
144 \r
145                         // Cut\r
146                         case CKEDITOR.CTRL + 88 :               // CTRL+X\r
147                         case CKEDITOR.SHIFT + 46 :              // SHIFT+DEL\r
148 \r
149                                 // Save Undo snapshot.\r
150                                 var editor = this;\r
151                                 this.fire( 'saveSnapshot' );            // Save before paste\r
152                                 setTimeout( function()\r
153                                         {\r
154                                                 editor.fire( 'saveSnapshot' );          // Save after paste\r
155                                         }, 0 );\r
156                 }\r
157         };\r
158 \r
159         function cancel( evt ) { evt.cancel(); }\r
160 \r
161         // Allow to peek clipboard content by redirecting the\r
162         // pasting content into a temporary bin and grab the content of it.\r
163         function getClipboardData( evt, mode, callback )\r
164         {\r
165                 var doc = this.document;\r
166 \r
167                 // Avoid recursions on 'paste' event or consequent paste too fast. (#5730)\r
168                 if ( doc.getById( 'cke_pastebin' ) )\r
169                         return;\r
170 \r
171                 // If the browser supports it, get the data directly\r
172                 if ( mode == 'text' && evt.data && evt.data.$.clipboardData )\r
173                 {\r
174                         // evt.data.$.clipboardData.types contains all the flavours in Mac's Safari, but not on windows.\r
175                         var plain = evt.data.$.clipboardData.getData( 'text/plain' );\r
176                         if ( plain )\r
177                         {\r
178                                 evt.data.preventDefault();\r
179                                 callback( plain );\r
180                                 return;\r
181                         }\r
182                 }\r
183 \r
184                 var sel = this.getSelection(),\r
185                         range = new CKEDITOR.dom.range( doc );\r
186 \r
187                 // Create container to paste into\r
188                 var pastebin = new CKEDITOR.dom.element( mode == 'text' ? 'textarea' : CKEDITOR.env.webkit ? 'body' : 'div', doc );\r
189                 pastebin.setAttribute( 'id', 'cke_pastebin' );\r
190                 // Safari requires a filler node inside the div to have the content pasted into it. (#4882)\r
191                 CKEDITOR.env.webkit && pastebin.append( doc.createText( '\xa0' ) );\r
192                 doc.getBody().append( pastebin );\r
193 \r
194                 pastebin.setStyles(\r
195                         {\r
196                                 position : 'absolute',\r
197                                 // Position the bin exactly at the position of the selected element\r
198                                 // to avoid any subsequent document scroll.\r
199                                 top : sel.getStartElement().getDocumentPosition().y + 'px',\r
200                                 width : '1px',\r
201                                 height : '1px',\r
202                                 overflow : 'hidden'\r
203                         });\r
204 \r
205                 // It's definitely a better user experience if we make the paste-bin pretty unnoticed\r
206                 // by pulling it off the screen.\r
207                 pastebin.setStyle( this.config.contentsLangDirection == 'ltr' ? 'left' : 'right', '-1000px' );\r
208 \r
209                 var bms = sel.createBookmarks();\r
210 \r
211                 this.on( 'selectionChange', cancel, null, null, 0 );\r
212 \r
213                 // Turn off design mode temporarily before give focus to the paste bin.\r
214                 if ( mode == 'text' )\r
215                         pastebin.$.focus();\r
216                 else\r
217                 {\r
218                         range.setStartAt( pastebin, CKEDITOR.POSITION_AFTER_START );\r
219                         range.setEndAt( pastebin, CKEDITOR.POSITION_BEFORE_END );\r
220                         range.select( true );\r
221                 }\r
222 \r
223                 var editor  = this;\r
224                 // Wait a while and grab the pasted contents\r
225                 window.setTimeout( function()\r
226                 {\r
227                         // Restore properly the document focus. (#5684, #8849)\r
228                         editor.document.getBody().focus();\r
229 \r
230                         editor.removeListener( 'selectionChange', cancel );\r
231 \r
232                         // IE7: selection must go before removing paste bin. (#8691)\r
233                         if ( CKEDITOR.env.ie7Compat )\r
234                         {\r
235                                 sel.selectBookmarks( bms );\r
236                                 pastebin.remove();\r
237                         }\r
238                         // Webkit: selection must go after removing paste bin. (#8921)\r
239                         else\r
240                         {\r
241                                 pastebin.remove();\r
242                                 sel.selectBookmarks( bms );\r
243                         }\r
244 \r
245                         // Grab the HTML contents.\r
246                         // We need to look for a apple style wrapper on webkit it also adds\r
247                         // a div wrapper if you copy/paste the body of the editor.\r
248                         // Remove hidden div and restore selection.\r
249                         var bogusSpan;\r
250                         pastebin = ( CKEDITOR.env.webkit\r
251                                                  && ( bogusSpan = pastebin.getFirst() )\r
252                                                  && ( bogusSpan.is && bogusSpan.hasClass( 'Apple-style-span' ) ) ?\r
253                                                         bogusSpan : pastebin );\r
254 \r
255                         callback( pastebin[ 'get' + ( mode == 'text' ? 'Value' : 'Html' ) ]() );\r
256                 }, 0 );\r
257         }\r
258 \r
259         // Cutting off control type element in IE standards breaks the selection entirely. (#4881)\r
260         function fixCut( editor )\r
261         {\r
262                 if ( !CKEDITOR.env.ie || CKEDITOR.env.quirks )\r
263                         return;\r
264 \r
265                 var sel = editor.getSelection();\r
266                 var control;\r
267                 if( ( sel.getType() == CKEDITOR.SELECTION_ELEMENT ) && ( control = sel.getSelectedElement() ) )\r
268                 {\r
269                         var range = sel.getRanges()[ 0 ];\r
270                         var dummy = editor.document.createText( '' );\r
271                         dummy.insertBefore( control );\r
272                         range.setStartBefore( dummy );\r
273                         range.setEndAfter( control );\r
274                         sel.selectRanges( [ range ] );\r
275 \r
276                         // Clear up the fix if the paste wasn't succeeded.\r
277                         setTimeout( function()\r
278                         {\r
279                                 // Element still online?\r
280                                 if ( control.getParent() )\r
281                                 {\r
282                                         dummy.remove();\r
283                                         sel.selectElement( control );\r
284                                 }\r
285                         }, 0 );\r
286                 }\r
287         }\r
288 \r
289         var depressBeforeEvent,\r
290                 inReadOnly;\r
291         function stateFromNamedCommand( command, editor )\r
292         {\r
293                 var retval;\r
294 \r
295                 if ( inReadOnly && command in { Paste : 1, Cut : 1 } )\r
296                         return CKEDITOR.TRISTATE_DISABLED;\r
297 \r
298                 if ( command == 'Paste' )\r
299                 {\r
300                         // IE Bug: queryCommandEnabled('paste') fires also 'beforepaste(copy/cut)',\r
301                         // guard to distinguish from the ordinary sources (either\r
302                         // keyboard paste or execCommand) (#4874).\r
303                         CKEDITOR.env.ie && ( depressBeforeEvent = 1 );\r
304                         try\r
305                         {\r
306                                 // Always return true for Webkit (which always returns false).\r
307                                 retval = editor.document.$.queryCommandEnabled( command ) || CKEDITOR.env.webkit;\r
308                         }\r
309                         catch( er ) {}\r
310                         depressBeforeEvent = 0;\r
311                 }\r
312                 // Cut, Copy - check if the selection is not empty\r
313                 else\r
314                 {\r
315                         var sel = editor.getSelection(),\r
316                                 ranges = sel && sel.getRanges();\r
317                         retval = sel && !( ranges.length == 1 && ranges[ 0 ].collapsed );\r
318                 }\r
319 \r
320                 return retval ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED;\r
321         }\r
322 \r
323         function setToolbarStates()\r
324         {\r
325                 if ( this.mode != 'wysiwyg' )\r
326                         return;\r
327 \r
328                 var pasteState = stateFromNamedCommand( 'Paste', this );\r
329 \r
330                 this.getCommand( 'cut' ).setState( stateFromNamedCommand( 'Cut', this ) );\r
331                 this.getCommand( 'copy' ).setState( stateFromNamedCommand( 'Copy', this ) );\r
332                 this.getCommand( 'paste' ).setState( pasteState );\r
333                 this.fire( 'pasteState', pasteState );\r
334         }\r
335 \r
336         // Register the plugin.\r
337         CKEDITOR.plugins.add( 'clipboard',\r
338                 {\r
339                         requires : [ 'dialog', 'htmldataprocessor' ],\r
340                         init : function( editor )\r
341                         {\r
342                                 // Inserts processed data into the editor at the end of the\r
343                                 // events chain.\r
344                                 editor.on( 'paste', function( evt )\r
345                                         {\r
346                                                 var data = evt.data;\r
347                                                 if ( data[ 'html' ] )\r
348                                                         editor.insertHtml( data[ 'html' ] );\r
349                                                 else if ( data[ 'text' ] )\r
350                                                         editor.insertText( data[ 'text' ] );\r
351 \r
352                                                 setTimeout( function () { editor.fire( 'afterPaste' ); }, 0 );\r
353 \r
354                                         }, null, null, 1000 );\r
355 \r
356                                 editor.on( 'pasteDialog', function( evt )\r
357                                         {\r
358                                                 setTimeout( function()\r
359                                                 {\r
360                                                         // Open default paste dialog.\r
361                                                         editor.openDialog( 'paste' );\r
362                                                 }, 0 );\r
363                                         });\r
364 \r
365                                 editor.on( 'pasteState', function( evt )\r
366                                         {\r
367                                                 editor.getCommand( 'paste' ).setState( evt.data );\r
368                                         });\r
369 \r
370                                 function addButtonCommand( buttonName, commandName, command, ctxMenuOrder )\r
371                                 {\r
372                                         var lang = editor.lang[ commandName ];\r
373 \r
374                                         editor.addCommand( commandName, command );\r
375                                         editor.ui.addButton( buttonName,\r
376                                                 {\r
377                                                         label : lang,\r
378                                                         command : commandName\r
379                                                 });\r
380 \r
381                                         // If the "menu" plugin is loaded, register the menu item.\r
382                                         if ( editor.addMenuItems )\r
383                                         {\r
384                                                 editor.addMenuItem( commandName,\r
385                                                         {\r
386                                                                 label : lang,\r
387                                                                 command : commandName,\r
388                                                                 group : 'clipboard',\r
389                                                                 order : ctxMenuOrder\r
390                                                         });\r
391                                         }\r
392                                 }\r
393 \r
394                                 addButtonCommand( 'Cut', 'cut', new cutCopyCmd( 'cut' ), 1 );\r
395                                 addButtonCommand( 'Copy', 'copy', new cutCopyCmd( 'copy' ), 4 );\r
396                                 addButtonCommand( 'Paste', 'paste', pasteCmd, 8 );\r
397 \r
398                                 CKEDITOR.dialog.add( 'paste', CKEDITOR.getUrl( this.path + 'dialogs/paste.js' ) );\r
399 \r
400                                 editor.on( 'key', onKey, editor );\r
401 \r
402                                 // We'll be catching all pasted content in one line, regardless of whether the\r
403                                 // it's introduced by a document command execution (e.g. toolbar buttons) or\r
404                                 // user paste behaviors. (e.g. Ctrl-V)\r
405                                 editor.on( 'contentDom', function()\r
406                                 {\r
407                                         var body = editor.document.getBody();\r
408 \r
409                                         // Intercept the paste before it actually takes place.\r
410                                         body.on( !CKEDITOR.env.ie ? 'paste' : 'beforepaste', function( evt )\r
411                                                 {\r
412                                                         if ( depressBeforeEvent )\r
413                                                                 return;\r
414 \r
415                                                         // Dismiss the (wrong) 'beforepaste' event fired on toolbar menu open.\r
416                                                         var domEvent = evt.data && evt.data.$;\r
417                                                         if ( CKEDITOR.env.ie && domEvent && !domEvent.ctrlKey )\r
418                                                                 return;\r
419 \r
420                                                         // Fire 'beforePaste' event so clipboard flavor get customized\r
421                                                         // by other plugins.\r
422                                                         var eventData =  { mode : 'html' };\r
423                                                         editor.fire( 'beforePaste', eventData );\r
424 \r
425                                                         getClipboardData.call( editor, evt, eventData.mode, function ( data )\r
426                                                         {\r
427                                                                 // The very last guard to make sure the\r
428                                                                 // paste has successfully happened.\r
429                                                                 if ( !( data = CKEDITOR.tools.trim( data.replace( /<span[^>]+data-cke-bookmark[^<]*?<\/span>/ig,'' ) ) ) )\r
430                                                                         return;\r
431 \r
432                                                                 var dataTransfer = {};\r
433                                                                 dataTransfer[ eventData.mode ] = data;\r
434                                                                 editor.fire( 'paste', dataTransfer );\r
435                                                         } );\r
436                                                 });\r
437 \r
438                                         if ( CKEDITOR.env.ie )\r
439                                         {\r
440                                                 // Dismiss the (wrong) 'beforepaste' event fired on context menu open. (#7953)\r
441                                                 body.on( 'contextmenu', function()\r
442                                                 {\r
443                                                         depressBeforeEvent = 1;\r
444                                                         // Important: The following timeout will be called only after menu closed.\r
445                                                         setTimeout( function() { depressBeforeEvent = 0; }, 0 );\r
446                                                 } );\r
447 \r
448                                                 // Handle IE's late coming "paste" event when pasting from\r
449                                                 // browser toolbar/context menu.\r
450                                                 body.on( 'paste', function( evt )\r
451                                                 {\r
452                                                         if ( !editor.document.getById( 'cke_pastebin' ) )\r
453                                                         {\r
454                                                                 // Prevent native paste.\r
455                                                                 evt.data.preventDefault();\r
456 \r
457                                                                 depressBeforeEvent = 0;\r
458                                                                 // Resort to the paste command.\r
459                                                                 pasteCmd.exec( editor );\r
460                                                         }\r
461                                                 } );\r
462                                         }\r
463 \r
464                                         body.on( 'beforecut', function() { !depressBeforeEvent && fixCut( editor ); } );\r
465 \r
466                                         body.on( 'mouseup', function(){ setTimeout( function(){ setToolbarStates.call( editor ); }, 0 ); }, editor );\r
467                                         body.on( 'keyup', setToolbarStates, editor );\r
468                                 });\r
469 \r
470                                 // For improved performance, we're checking the readOnly state on selectionChange instead of hooking a key event for that.\r
471                                 editor.on( 'selectionChange', function( evt )\r
472                                 {\r
473                                         inReadOnly = evt.data.selection.getRanges()[ 0 ].checkReadOnly();\r
474                                         setToolbarStates.call( editor );\r
475                                 });\r
476 \r
477                                 // If the "contextmenu" plugin is loaded, register the listeners.\r
478                                 if ( editor.contextMenu )\r
479                                 {\r
480                                         editor.contextMenu.addListener( function( element, selection )\r
481                                                 {\r
482                                                         var readOnly = selection.getRanges()[ 0 ].checkReadOnly();\r
483                                                         return {\r
484                                                                 cut : stateFromNamedCommand( 'Cut', editor ),\r
485                                                                 copy : stateFromNamedCommand( 'Copy', editor ),\r
486                                                                 paste : stateFromNamedCommand( 'Paste', editor )\r
487                                                         };\r
488                                                 });\r
489                                 }\r
490                         }\r
491                 });\r
492 })();\r
493 \r
494 /**\r
495  * Fired when a clipboard operation is about to be taken into the editor.\r
496  * Listeners can manipulate the data to be pasted before having it effectively\r
497  * inserted into the document.\r
498  * @name CKEDITOR.editor#paste\r
499  * @since 3.1\r
500  * @event\r
501  * @param {String} [data.html] The HTML data to be pasted. If not available, e.data.text will be defined.\r
502  * @param {String} [data.text] The plain text data to be pasted, available when plain text operations are to used. If not available, e.data.html will be defined.\r
503  */\r
504 \r
505 /**\r
506  * Internal event to open the Paste dialog\r
507  * @name CKEDITOR.editor#pasteDialog\r
508  * @event\r
509  */\r