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