]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blob - sources/plugins/clipboard/plugin.js
Update to 4.7.3
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / plugins / clipboard / plugin.js
1 /**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 /**
7 * @ignore
8 * File overview: Clipboard support.
9 */
10
11 //
12 // COPY & PASTE EXECUTION FLOWS:
13 // -- CTRL+C
14 // * if ( isCustomCopyCutSupported )
15 // * dataTransfer.setData( 'text/html', getSelectedHtml )
16 // * else
17 // * browser's default behavior
18 // -- CTRL+X
19 // * listen onKey (onkeydown)
20 // * fire 'saveSnapshot' on editor
21 // * if ( isCustomCopyCutSupported )
22 // * dataTransfer.setData( 'text/html', getSelectedHtml )
23 // * extractSelectedHtml // remove selected contents
24 // * else
25 // * browser's default behavior
26 // * deferred second 'saveSnapshot' event
27 // -- CTRL+V
28 // * listen onKey (onkeydown)
29 // * simulate 'beforepaste' for non-IEs on editable
30 // * listen 'onpaste' on editable ('onbeforepaste' for IE)
31 // * fire 'beforePaste' on editor
32 // * if ( !canceled && ( htmlInDataTransfer || !external paste) && dataTransfer is not empty ) getClipboardDataByPastebin
33 // * fire 'paste' on editor
34 // * !canceled && fire 'afterPaste' on editor
35 // -- Copy command
36 // * tryToCutCopy
37 // * execCommand
38 // * !success && notification
39 // -- Cut command
40 // * fixCut
41 // * tryToCutCopy
42 // * execCommand
43 // * !success && notification
44 // -- Paste command
45 // * fire 'paste' on editable ('beforepaste' for IE)
46 // * !canceled && execCommand 'paste'
47 // -- Paste from native context menu & menubar
48 // (Fx & Webkits are handled in 'paste' default listener.
49 // Opera cannot be handled at all because it doesn't fire any events
50 // Special treatment is needed for IE, for which is this part of doc)
51 // * listen 'onpaste'
52 // * cancel native event
53 // * fire 'beforePaste' on editor
54 // * if ( !canceled && ( htmlInDataTransfer || !external paste) && dataTransfer is not empty ) getClipboardDataByPastebin
55 // * execIECommand( 'paste' ) -> this fires another 'paste' event, so cancel it
56 // * fire 'paste' on editor
57 // * !canceled && fire 'afterPaste' on editor
58 //
59 //
60 // PASTE EVENT - PREPROCESSING:
61 // -- Possible dataValue types: auto, text, html.
62 // -- Possible dataValue contents:
63 // * text (possible \n\r)
64 // * htmlified text (text + br,div,p - no presentational markup & attrs - depends on browser)
65 // * html
66 // -- Possible flags:
67 // * htmlified - if true then content is a HTML even if no markup inside. This flag is set
68 // for content from editable pastebins, because they 'htmlify' pasted content.
69 //
70 // -- Type: auto:
71 // * content: htmlified text -> filter, unify text markup (brs, ps, divs), set type: text
72 // * content: html -> filter, set type: html
73 // -- Type: text:
74 // * content: htmlified text -> filter, unify text markup
75 // * content: html -> filter, strip presentational markup, unify text markup
76 // -- Type: html:
77 // * content: htmlified text -> filter, unify text markup
78 // * content: html -> filter
79 //
80 // -- Phases:
81 // * if dataValue is empty copy data from dataTransfer to dataValue (priority 1)
82 // * filtering (priorities 3-5) - e.g. pastefromword filters
83 // * content type sniffing (priority 6)
84 // * markup transformations for text (priority 6)
85 //
86 // DRAG & DROP EXECUTION FLOWS:
87 // -- Drag
88 // * save to the global object:
89 // * drag timestamp (with 'cke-' prefix),
90 // * selected html,
91 // * drag range,
92 // * editor instance.
93 // * put drag timestamp into event.dataTransfer.text
94 // -- Drop
95 // * if events text == saved timestamp && editor == saved editor
96 // internal drag & drop occurred
97 // * getRangeAtDropPosition
98 // * create bookmarks for drag and drop ranges starting from the end of the document
99 // * dragRange.deleteContents()
100 // * fire 'paste' with saved html and drop range
101 // * if events text == saved timestamp && editor != saved editor
102 // cross editor drag & drop occurred
103 // * getRangeAtDropPosition
104 // * fire 'paste' with saved html
105 // * dragRange.deleteContents()
106 // * FF: refreshCursor on afterPaste
107 // * if events text != saved timestamp
108 // drop form external source occurred
109 // * getRangeAtDropPosition
110 // * if event contains html data then fire 'paste' with html
111 // * else if event contains text data then fire 'paste' with encoded text
112 // * FF: refreshCursor on afterPaste
113
114 'use strict';
115
116 ( function() {
117 // Register the plugin.
118 CKEDITOR.plugins.add( 'clipboard', {
119 requires: 'notification,toolbar',
120 // jscs:disable maximumLineLength
121 lang: 'af,ar,az,bg,bn,bs,ca,cs,cy,da,de,de-ch,el,en,en-au,en-ca,en-gb,eo,es,es-mx,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,oc,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
122 // jscs:enable maximumLineLength
123 icons: 'copy,copy-rtl,cut,cut-rtl,paste,paste-rtl', // %REMOVE_LINE_CORE%
124 hidpi: true, // %REMOVE_LINE_CORE%
125 init: function( editor ) {
126 var filterType,
127 filtersFactory = filtersFactoryFactory();
128
129 if ( editor.config.forcePasteAsPlainText ) {
130 filterType = 'plain-text';
131 } else if ( editor.config.pasteFilter ) {
132 filterType = editor.config.pasteFilter;
133 }
134 // On Webkit the pasteFilter defaults 'semantic-content' because pasted data is so terrible
135 // that it must be always filtered.
136 else if ( CKEDITOR.env.webkit && !( 'pasteFilter' in editor.config ) ) {
137 filterType = 'semantic-content';
138 }
139
140 editor.pasteFilter = filtersFactory.get( filterType );
141
142 initPasteClipboard( editor );
143 initDragDrop( editor );
144
145 // Convert image file (if present) to base64 string for Firefox. Do it as the first
146 // step as the conversion is asynchronous and should hold all further paste processing.
147 if ( CKEDITOR.env.gecko ) {
148 var supportedImageTypes = [ 'image/png', 'image/jpeg', 'image/gif' ],
149 latestId;
150
151 editor.on( 'paste', function( evt ) {
152 var dataObj = evt.data,
153 data = dataObj.dataValue,
154 dataTransfer = dataObj.dataTransfer;
155
156 // If data empty check for image content inside data transfer. http://dev.ckeditor.com/ticket/16705
157 if ( !data && dataObj.method == 'paste' && dataTransfer && dataTransfer.getFilesCount() == 1 && latestId != dataTransfer.id ) {
158 var file = dataTransfer.getFile( 0 );
159
160 if ( CKEDITOR.tools.indexOf( supportedImageTypes, file.type ) != -1 ) {
161 var fileReader = new FileReader();
162
163 // Convert image file to img tag with base64 image.
164 fileReader.addEventListener( 'load', function() {
165 evt.data.dataValue = '<img src="' + fileReader.result + '" />';
166 editor.fire( 'paste', evt.data );
167 }, false );
168
169 // Proceed with normal flow if reading file was aborted.
170 fileReader.addEventListener( 'abort', function() {
171 editor.fire( 'paste', evt.data );
172 }, false );
173
174 // Proceed with normal flow if reading file failed.
175 fileReader.addEventListener( 'error', function() {
176 editor.fire( 'paste', evt.data );
177 }, false );
178
179 fileReader.readAsDataURL( file );
180
181 latestId = dataObj.dataTransfer.id;
182
183 evt.stop();
184 }
185 }
186 }, null, null, 1 );
187 }
188
189 editor.on( 'paste', function( evt ) {
190 // Init `dataTransfer` if `paste` event was fired without it, so it will be always available.
191 if ( !evt.data.dataTransfer ) {
192 evt.data.dataTransfer = new CKEDITOR.plugins.clipboard.dataTransfer();
193 }
194
195 // If dataValue is already set (manually or by paste bin), so do not override it.
196 if ( evt.data.dataValue ) {
197 return;
198 }
199
200 var dataTransfer = evt.data.dataTransfer,
201 // IE support only text data and throws exception if we try to get html data.
202 // This html data object may also be empty if we drag content of the textarea.
203 value = dataTransfer.getData( 'text/html' );
204
205 if ( value ) {
206 evt.data.dataValue = value;
207 evt.data.type = 'html';
208 } else {
209 // Try to get text data otherwise.
210 value = dataTransfer.getData( 'text/plain' );
211
212 if ( value ) {
213 evt.data.dataValue = editor.editable().transformPlainTextToHtml( value );
214 evt.data.type = 'text';
215 }
216 }
217 }, null, null, 1 );
218
219 editor.on( 'paste', function( evt ) {
220 var data = evt.data.dataValue,
221 blockElements = CKEDITOR.dtd.$block;
222
223 // Filter webkit garbage.
224 if ( data.indexOf( 'Apple-' ) > -1 ) {
225 // Replace special webkit's &nbsp; with simple space, because webkit
226 // produces them even for normal spaces.
227 data = data.replace( /<span class="Apple-converted-space">&nbsp;<\/span>/gi, ' ' );
228
229 // Strip <span> around white-spaces when not in forced 'html' content type.
230 // This spans are created only when pasting plain text into Webkit,
231 // but for safety reasons remove them always.
232 if ( evt.data.type != 'html' ) {
233 data = data.replace( /<span class="Apple-tab-span"[^>]*>([^<]*)<\/span>/gi, function( all, spaces ) {
234 // Replace tabs with 4 spaces like Fx does.
235 return spaces.replace( /\t/g, '&nbsp;&nbsp; &nbsp;' );
236 } );
237 }
238
239 // This br is produced only when copying & pasting HTML content.
240 if ( data.indexOf( '<br class="Apple-interchange-newline">' ) > -1 ) {
241 evt.data.startsWithEOL = 1;
242 evt.data.preSniffing = 'html'; // Mark as not text.
243 data = data.replace( /<br class="Apple-interchange-newline">/, '' );
244 }
245
246 // Remove all other classes.
247 data = data.replace( /(<[^>]+) class="Apple-[^"]*"/gi, '$1' );
248 }
249
250 // Strip editable that was copied from inside. (http://dev.ckeditor.com/ticket/9534)
251 if ( data.match( /^<[^<]+cke_(editable|contents)/i ) ) {
252 var tmp,
253 editable_wrapper,
254 wrapper = new CKEDITOR.dom.element( 'div' );
255
256 wrapper.setHtml( data );
257 // Verify for sure and check for nested editor UI parts. (http://dev.ckeditor.com/ticket/9675)
258 while ( wrapper.getChildCount() == 1 &&
259 ( tmp = wrapper.getFirst() ) &&
260 tmp.type == CKEDITOR.NODE_ELEMENT && // Make sure first-child is element.
261 ( tmp.hasClass( 'cke_editable' ) || tmp.hasClass( 'cke_contents' ) ) ) {
262 wrapper = editable_wrapper = tmp;
263 }
264
265 // If editable wrapper was found strip it and bogus <br> (added on FF).
266 if ( editable_wrapper )
267 data = editable_wrapper.getHtml().replace( /<br>$/i, '' );
268 }
269
270 if ( CKEDITOR.env.ie ) {
271 // &nbsp; <p> -> <p> (br.cke-pasted-remove will be removed later)
272 data = data.replace( /^&nbsp;(?: |\r\n)?<(\w+)/g, function( match, elementName ) {
273 if ( elementName.toLowerCase() in blockElements ) {
274 evt.data.preSniffing = 'html'; // Mark as not a text.
275 return '<' + elementName;
276 }
277 return match;
278 } );
279 } else if ( CKEDITOR.env.webkit ) {
280 // </p><div><br></div> -> </p><br>
281 // We don't mark br, because this situation can happen for htmlified text too.
282 data = data.replace( /<\/(\w+)><div><br><\/div>$/, function( match, elementName ) {
283 if ( elementName in blockElements ) {
284 evt.data.endsWithEOL = 1;
285 return '</' + elementName + '>';
286 }
287 return match;
288 } );
289 } else if ( CKEDITOR.env.gecko ) {
290 // Firefox adds bogus <br> when user pasted text followed by space(s).
291 data = data.replace( /(\s)<br>$/, '$1' );
292 }
293
294 evt.data.dataValue = data;
295 }, null, null, 3 );
296
297 editor.on( 'paste', function( evt ) {
298 var dataObj = evt.data,
299 type = editor._.nextPasteType || dataObj.type,
300 data = dataObj.dataValue,
301 trueType,
302 // Default is 'html'.
303 defaultType = editor.config.clipboard_defaultContentType || 'html',
304 transferType = dataObj.dataTransfer.getTransferType( editor );
305
306 // If forced type is 'html' we don't need to know true data type.
307 if ( type == 'html' || dataObj.preSniffing == 'html' ) {
308 trueType = 'html';
309 } else {
310 trueType = recogniseContentType( data );
311 }
312
313 delete editor._.nextPasteType;
314
315 // Unify text markup.
316 if ( trueType == 'htmlifiedtext' ) {
317 data = htmlifiedTextHtmlification( editor.config, data );
318 }
319
320 // Strip presentational markup & unify text markup.
321 // Forced plain text.
322 // Note: we do not check dontFilter option in this case, because forcePAPT was implemented
323 // before pasteFilter and pasteFilter is automatically used on Webkit&Blink since 4.5, so
324 // forcePAPT should have priority as it had before 4.5.
325 if ( type == 'text' && trueType == 'html' ) {
326 data = filterContent( editor, data, filtersFactory.get( 'plain-text' ) );
327 }
328 // External paste and pasteFilter exists and filtering isn't disabled.
329 else if ( transferType == CKEDITOR.DATA_TRANSFER_EXTERNAL && editor.pasteFilter && !dataObj.dontFilter ) {
330 data = filterContent( editor, data, editor.pasteFilter );
331 }
332
333 if ( dataObj.startsWithEOL ) {
334 data = '<br data-cke-eol="1">' + data;
335 }
336 if ( dataObj.endsWithEOL ) {
337 data += '<br data-cke-eol="1">';
338 }
339
340 if ( type == 'auto' ) {
341 type = ( trueType == 'html' || defaultType == 'html' ) ? 'html' : 'text';
342 }
343
344 dataObj.type = type;
345 dataObj.dataValue = data;
346 delete dataObj.preSniffing;
347 delete dataObj.startsWithEOL;
348 delete dataObj.endsWithEOL;
349 }, null, null, 6 );
350
351 // Inserts processed data into the editor at the end of the
352 // events chain.
353 editor.on( 'paste', function( evt ) {
354 var data = evt.data;
355
356 if ( data.dataValue ) {
357 editor.insertHtml( data.dataValue, data.type, data.range );
358
359 // Defer 'afterPaste' so all other listeners for 'paste' will be fired first.
360 // Fire afterPaste only if paste inserted some HTML.
361 setTimeout( function() {
362 editor.fire( 'afterPaste' );
363 }, 0 );
364 }
365 }, null, null, 1000 );
366 }
367 } );
368
369 function firePasteEvents( editor, data, withBeforePaste ) {
370 if ( !data.type ) {
371 data.type = 'auto';
372 }
373
374 if ( withBeforePaste ) {
375 // Fire 'beforePaste' event so clipboard flavor get customized
376 // by other plugins.
377 if ( editor.fire( 'beforePaste', data ) === false )
378 return false; // Event canceled
379 }
380
381 // Do not fire paste if there is no data (dataValue and dataTranfser are empty).
382 // This check should be done after firing 'beforePaste' because for native paste
383 // 'beforePaste' is by default fired even for empty clipboard.
384 if ( !data.dataValue && data.dataTransfer.isEmpty() ) {
385 return false;
386 }
387
388 if ( !data.dataValue ) {
389 data.dataValue = '';
390 }
391
392 // Because of FF bug we need to use this hack, otherwise cursor is hidden
393 // or it is not possible to move it (http://dev.ckeditor.com/ticket/12420).
394 // Also, check that editor.toolbox exists, because the toolbar plugin might not be loaded (http://dev.ckeditor.com/ticket/13305).
395 if ( CKEDITOR.env.gecko && data.method == 'drop' && editor.toolbox ) {
396 editor.once( 'afterPaste', function() {
397 editor.toolbox.focus();
398 } );
399 }
400
401 return editor.fire( 'paste', data );
402 }
403
404 function initPasteClipboard( editor ) {
405 var clipboard = CKEDITOR.plugins.clipboard,
406 preventBeforePasteEvent = 0,
407 preventPasteEvent = 0,
408 inReadOnly = 0;
409
410 addListeners();
411 addButtonsCommands();
412
413 /**
414 * Gets clipboard data by directly accessing the clipboard (IE only).
415 *
416 * editor.getClipboardData( function( data ) {
417 * if ( data )
418 * alert( data.type + ' ' + data.dataValue );
419 * } );
420 *
421 * @member CKEDITOR.editor
422 * @param {Function/Object} callbackOrOptions For function, see the `callback` parameter documentation. The object was used before 4.7.0 with the `title` property, to set the paste dialog's title.
423 * @param {Function} callback A function that will be executed with the `data` property of the
424 * {@link CKEDITOR.editor#event-paste paste event} or `null` if none of the capturing methods succeeded.
425 * Since 4.7.0 the `callback` should be provided as a first argument, just like in the example above. This parameter will be removed in
426 * an upcoming major release.
427 */
428 editor.getClipboardData = function( callbackOrOptions, callback ) {
429 // Options are optional - args shift.
430 if ( !callback ) {
431 callback = callbackOrOptions;
432 callbackOrOptions = null;
433 }
434
435 // Listen with maximum priority to handle content before everyone else.
436 // This callback will handle paste event that will be fired if direct
437 // access to the clipboard succeed in IE.
438 editor.on( 'paste', onPaste, null, null, 0 );
439
440 // If command didn't succeed (only IE allows to access clipboard and only if
441 // user agrees) invoke callback with null, meaning that paste is not blocked.
442 if ( getClipboardDataDirectly() === false ) {
443 // Direct access to the clipboard wasn't successful so remove listener.
444 editor.removeListener( 'paste', onPaste );
445
446 callback( null );
447 }
448
449 function onPaste( evt ) {
450 evt.removeListener();
451 evt.cancel();
452 callback( evt.data );
453 }
454 };
455
456 function addButtonsCommands() {
457 addButtonCommand( 'Cut', 'cut', createCutCopyCmd( 'cut' ), 10, 1 );
458 addButtonCommand( 'Copy', 'copy', createCutCopyCmd( 'copy' ), 20, 4 );
459 addButtonCommand( 'Paste', 'paste', createPasteCmd(), 30, 8 );
460
461 function addButtonCommand( buttonName, commandName, command, toolbarOrder, ctxMenuOrder ) {
462 var lang = editor.lang.clipboard[ commandName ];
463
464 editor.addCommand( commandName, command );
465 editor.ui.addButton && editor.ui.addButton( buttonName, {
466 label: lang,
467 command: commandName,
468 toolbar: 'clipboard,' + toolbarOrder
469 } );
470
471 // If the "menu" plugin is loaded, register the menu item.
472 if ( editor.addMenuItems ) {
473 editor.addMenuItem( commandName, {
474 label: lang,
475 command: commandName,
476 group: 'clipboard',
477 order: ctxMenuOrder
478 } );
479 }
480 }
481 }
482
483 function addListeners() {
484 editor.on( 'key', onKey );
485 editor.on( 'contentDom', addPasteListenersToEditable );
486
487 // For improved performance, we're checking the readOnly state on selectionChange instead of hooking a key event for that.
488 editor.on( 'selectionChange', function( evt ) {
489 inReadOnly = evt.data.selection.getRanges()[ 0 ].checkReadOnly();
490 setToolbarStates();
491 } );
492
493 // If the "contextmenu" plugin is loaded, register the listeners.
494 if ( editor.contextMenu ) {
495 editor.contextMenu.addListener( function( element, selection ) {
496 inReadOnly = selection.getRanges()[ 0 ].checkReadOnly();
497 return {
498 cut: stateFromNamedCommand( 'cut' ),
499 copy: stateFromNamedCommand( 'copy' ),
500 paste: stateFromNamedCommand( 'paste' )
501 };
502 } );
503 }
504 }
505
506 // Add events listeners to editable.
507 function addPasteListenersToEditable() {
508 var editable = editor.editable();
509
510 if ( CKEDITOR.plugins.clipboard.isCustomCopyCutSupported ) {
511 var initOnCopyCut = function( evt ) {
512 // If user tries to cut in read-only editor, we must prevent default action. (http://dev.ckeditor.com/ticket/13872)
513 if ( !editor.readOnly || evt.name != 'cut' ) {
514 clipboard.initPasteDataTransfer( evt, editor );
515 }
516 evt.data.preventDefault();
517 };
518
519 editable.on( 'copy', initOnCopyCut );
520 editable.on( 'cut', initOnCopyCut );
521
522 // Delete content with the low priority so one can overwrite cut data.
523 editable.on( 'cut', function() {
524 // If user tries to cut in read-only editor, we must prevent default action. (http://dev.ckeditor.com/ticket/13872)
525 if ( !editor.readOnly ) {
526 editor.extractSelectedHtml();
527 }
528 }, null, null, 999 );
529 }
530
531 // We'll be catching all pasted content in one line, regardless of whether
532 // it's introduced by a document command execution (e.g. toolbar buttons) or
533 // user paste behaviors (e.g. CTRL+V).
534 editable.on( clipboard.mainPasteEvent, function( evt ) {
535 if ( clipboard.mainPasteEvent == 'beforepaste' && preventBeforePasteEvent ) {
536 return;
537 }
538
539 // If you've just asked yourself why preventPasteEventNow() is not here, but
540 // in listener for CTRL+V and exec method of 'paste' command
541 // you've asked the same question we did.
542 //
543 // THE ANSWER:
544 //
545 // First thing to notice - this answer makes sense only for IE,
546 // because other browsers don't listen for 'paste' event.
547 //
548 // What would happen if we move preventPasteEventNow() here?
549 // For:
550 // * CTRL+V - IE fires 'beforepaste', so we prevent 'paste' and pasteDataFromClipboard(). OK.
551 // * editor.execCommand( 'paste' ) - we fire 'beforepaste', so we prevent
552 // 'paste' and pasteDataFromClipboard() and doc.execCommand( 'Paste' ). OK.
553 // * native context menu - IE fires 'beforepaste', so we prevent 'paste', but unfortunately
554 // on IE we fail with pasteDataFromClipboard() here, because of... we don't know why, but
555 // we just fail, so... we paste nothing. FAIL.
556 // * native menu bar - the same as for native context menu.
557 //
558 // But don't you know any way to distinguish first two cases from last two?
559 // Only one - special flag set in CTRL+V handler and exec method of 'paste'
560 // command. And that's what we did using preventPasteEventNow().
561
562 pasteDataFromClipboard( evt );
563 } );
564
565 // It's not possible to clearly handle all four paste methods (ctrl+v, native menu bar
566 // native context menu, editor's command) in one 'paste/beforepaste' event in IE.
567 //
568 // For ctrl+v & editor's command it's easy to handle pasting in 'beforepaste' listener,
569 // so we do this. For another two methods it's better to use 'paste' event.
570 //
571 // 'paste' is always being fired after 'beforepaste' (except of weird one on opening native
572 // context menu), so for two methods handled in 'beforepaste' we're canceling 'paste'
573 // using preventPasteEvent state.
574 //
575 // 'paste' event in IE is being fired before getClipboardDataByPastebin executes its callback.
576 //
577 // QUESTION: Why didn't you handle all 4 paste methods in handler for 'paste'?
578 // Wouldn't this just be simpler?
579 // ANSWER: Then we would have to evt.data.preventDefault() only for native
580 // context menu and menu bar pastes. The same with execIECommand().
581 // That would force us to mark CTRL+V and editor's paste command with
582 // special flag, other than preventPasteEvent. But we still would have to
583 // have preventPasteEvent for the second event fired by execIECommand.
584 // Code would be longer and not cleaner.
585 if ( clipboard.mainPasteEvent == 'beforepaste' ) {
586 editable.on( 'paste', function( evt ) {
587 if ( preventPasteEvent ) {
588 return;
589 }
590
591 // Cancel next 'paste' event fired by execIECommand( 'paste' )
592 // at the end of this callback.
593 preventPasteEventNow();
594
595 // Prevent native paste.
596 evt.data.preventDefault();
597
598 pasteDataFromClipboard( evt );
599
600 // Force IE to paste content into pastebin so pasteDataFromClipboard will work.
601 execIECommand( 'paste' );
602 } );
603
604 // If mainPasteEvent is 'beforePaste' (IE before Edge),
605 // dismiss the (wrong) 'beforepaste' event fired on context/toolbar menu open. (http://dev.ckeditor.com/ticket/7953)
606 editable.on( 'contextmenu', preventBeforePasteEventNow, null, null, 0 );
607
608 editable.on( 'beforepaste', function( evt ) {
609 // Do not prevent event on CTRL+V and SHIFT+INS because it blocks paste (http://dev.ckeditor.com/ticket/11970).
610 if ( evt.data && !evt.data.$.ctrlKey && !evt.data.$.shiftKey )
611 preventBeforePasteEventNow();
612 }, null, null, 0 );
613 }
614
615 editable.on( 'beforecut', function() {
616 !preventBeforePasteEvent && fixCut( editor );
617 } );
618
619 var mouseupTimeout;
620
621 // Use editor.document instead of editable in non-IEs for observing mouseup
622 // since editable won't fire the event if selection process started within
623 // iframe and ended out of the editor (http://dev.ckeditor.com/ticket/9851).
624 editable.attachListener( CKEDITOR.env.ie ? editable : editor.document.getDocumentElement(), 'mouseup', function() {
625 mouseupTimeout = setTimeout( function() {
626 setToolbarStates();
627 }, 0 );
628 } );
629
630 // Make sure that deferred mouseup callback isn't executed after editor instance
631 // had been destroyed. This may happen when editor.destroy() is called in parallel
632 // with mouseup event (i.e. a button with onclick callback) (http://dev.ckeditor.com/ticket/10219).
633 editor.on( 'destroy', function() {
634 clearTimeout( mouseupTimeout );
635 } );
636
637 editable.on( 'keyup', setToolbarStates );
638 }
639
640 // Create object representing Cut or Copy commands.
641 function createCutCopyCmd( type ) {
642 return {
643 type: type,
644 canUndo: type == 'cut', // We can't undo copy to clipboard.
645 startDisabled: true,
646 fakeKeystroke: type == 'cut' ? CKEDITOR.CTRL + 88 /*X*/ : CKEDITOR.CTRL + 67 /*C*/,
647 exec: function() {
648 // Attempts to execute the Cut and Copy operations.
649 function tryToCutCopy( type ) {
650 if ( CKEDITOR.env.ie )
651 return execIECommand( type );
652
653 // non-IEs part
654 try {
655 // Other browsers throw an error if the command is disabled.
656 return editor.document.$.execCommand( type, false, null );
657 } catch ( e ) {
658 return false;
659 }
660 }
661
662 this.type == 'cut' && fixCut();
663
664 var success = tryToCutCopy( this.type );
665
666 if ( !success ) {
667 // Show cutError or copyError.
668 editor.showNotification( editor.lang.clipboard[ this.type + 'Error' ] ); // jshint ignore:line
669 }
670
671 return success;
672 }
673 };
674 }
675
676 function createPasteCmd() {
677 return {
678 // Snapshots are done manually by editable.insertXXX methods.
679 canUndo: false,
680 async: true,
681 fakeKeystroke: CKEDITOR.CTRL + 86 /*V*/,
682
683 /**
684 * The default implementation of the paste command.
685 *
686 * @private
687 * @param {CKEDITOR.editor} editor An instance of the editor where the command is being executed.
688 * @param {Object/String} data If `data` is a string, then it is considered content that is being pasted.
689 * Otherwise it is treated as an object with options.
690 * @param {Boolean/String} [data.notification=true] Content for a notification shown after an unsuccessful
691 * paste attempt. If `false`, the notification will not be displayed. This parameter was added in 4.7.0.
692 * @param {String} [data.type='html'] The type of pasted content. There are two allowed values:
693 * * 'html'
694 * * 'text'
695 * @param {String/Object} data.dataValue Content being pasted. If this parameter is an object, it
696 * is supposed to be a `data` property of the {@link CKEDITOR.editor#paste} event.
697 * @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer Data transfer instance connected
698 * with the current paste action.
699 * @member CKEDITOR.editor.commands.paste
700 */
701 exec: function( editor, data ) {
702 data = typeof data !== 'undefined' && data !== null ? data : {};
703
704 var cmd = this,
705 notification = typeof data.notification !== 'undefined' ? data.notification : true,
706 forcedType = data.type,
707 keystroke = CKEDITOR.tools.keystrokeToString( editor.lang.common.keyboard,
708 editor.getCommandKeystroke( this ) ),
709 msg = typeof notification === 'string' ? notification : editor.lang.clipboard.pasteNotification
710 .replace( /%1/, '<kbd aria-label="' + keystroke.aria + '">' + keystroke.display + '</kbd>' ),
711 pastedContent = typeof data === 'string' ? data : data.dataValue;
712
713 function callback( data, withBeforePaste ) {
714 withBeforePaste = typeof withBeforePaste !== 'undefined' ? withBeforePaste : true;
715
716 if ( data ) {
717 data.method = 'paste';
718
719 if ( !data.dataTransfer ) {
720 data.dataTransfer = clipboard.initPasteDataTransfer();
721 }
722
723 firePasteEvents( editor, data, withBeforePaste );
724 } else if ( notification ) {
725 editor.showNotification( msg, 'info', editor.config.clipboard_notificationDuration );
726 }
727
728 editor.fire( 'afterCommandExec', {
729 name: 'paste',
730 command: cmd,
731 returnValue: !!data
732 } );
733 }
734
735 // Force type for the next paste.
736 if ( forcedType ) {
737 editor._.nextPasteType = forcedType;
738 } else {
739 delete editor._.nextPasteType;
740 }
741
742 if ( typeof pastedContent === 'string' ) {
743 callback( {
744 dataValue: pastedContent
745 } );
746 } else {
747 editor.getClipboardData( callback );
748 }
749 }
750 };
751 }
752
753 function preventPasteEventNow() {
754 preventPasteEvent = 1;
755 // For safety reason we should wait longer than 0/1ms.
756 // We don't know how long execution of quite complex getClipboardData will take
757 // and in for example 'paste' listener execCommand() (which fires 'paste') is called
758 // after getClipboardData finishes.
759 // Luckily, it's impossible to immediately fire another 'paste' event we want to handle,
760 // because we only handle there native context menu and menu bar.
761 setTimeout( function() {
762 preventPasteEvent = 0;
763 }, 100 );
764 }
765
766 function preventBeforePasteEventNow() {
767 preventBeforePasteEvent = 1;
768 setTimeout( function() {
769 preventBeforePasteEvent = 0;
770 }, 10 );
771 }
772
773 // Tries to execute any of the paste, cut or copy commands in IE. Returns a
774 // boolean indicating that the operation succeeded.
775 // @param {String} command *LOWER CASED* name of command ('paste', 'cut', 'copy').
776 function execIECommand( command ) {
777 var doc = editor.document,
778 body = doc.getBody(),
779 enabled = false,
780 onExec = function() {
781 enabled = true;
782 };
783
784 // The following seems to be the only reliable way to detect that
785 // clipboard commands are enabled in IE. It will fire the
786 // onpaste/oncut/oncopy events only if the security settings allowed
787 // the command to execute.
788 body.on( command, onExec );
789
790 // IE7: document.execCommand has problem to paste into positioned element.
791 if ( CKEDITOR.env.version > 7 ) {
792 doc.$.execCommand( command );
793 } else {
794 doc.$.selection.createRange().execCommand( command );
795 }
796
797 body.removeListener( command, onExec );
798
799 return enabled;
800 }
801
802 // Cutting off control type element in IE standards breaks the selection entirely. (http://dev.ckeditor.com/ticket/4881)
803 function fixCut() {
804 if ( !CKEDITOR.env.ie || CKEDITOR.env.quirks )
805 return;
806
807 var sel = editor.getSelection(),
808 control, range, dummy;
809
810 if ( ( sel.getType() == CKEDITOR.SELECTION_ELEMENT ) && ( control = sel.getSelectedElement() ) ) {
811 range = sel.getRanges()[ 0 ];
812 dummy = editor.document.createText( '' );
813 dummy.insertBefore( control );
814 range.setStartBefore( dummy );
815 range.setEndAfter( control );
816 sel.selectRanges( [ range ] );
817
818 // Clear up the fix if the paste wasn't succeeded.
819 setTimeout( function() {
820 // Element still online?
821 if ( control.getParent() ) {
822 dummy.remove();
823 sel.selectElement( control );
824 }
825 }, 0 );
826 }
827 }
828
829 // Allow to peek clipboard content by redirecting the
830 // pasting content into a temporary bin and grab the content of it.
831 function getClipboardDataByPastebin( evt, callback ) {
832 var doc = editor.document,
833 editable = editor.editable(),
834 cancel = function( evt ) {
835 evt.cancel();
836 },
837 blurListener;
838
839 // Avoid recursions on 'paste' event or consequent paste too fast. (http://dev.ckeditor.com/ticket/5730)
840 if ( doc.getById( 'cke_pastebin' ) )
841 return;
842
843 var sel = editor.getSelection();
844 var bms = sel.createBookmarks();
845
846 // http://dev.ckeditor.com/ticket/11384. On IE9+ we use native selectionchange (i.e. editor#selectionCheck) to cache the most
847 // recent selection which we then lock on editable blur. See selection.js for more info.
848 // selectionchange fired before getClipboardDataByPastebin() cached selection
849 // before creating bookmark (cached selection will be invalid, because bookmarks modified the DOM),
850 // so we need to fire selectionchange one more time, to store current seleciton.
851 // Selection will be locked when we focus pastebin.
852 if ( CKEDITOR.env.ie )
853 sel.root.fire( 'selectionchange' );
854
855 // Create container to paste into.
856 // For rich content we prefer to use "body" since it holds
857 // the least possibility to be splitted by pasted content, while this may
858 // breaks the text selection on a frame-less editable, "div" would be
859 // the best one in that case.
860 // In another case on old IEs moving the selection into a "body" paste bin causes error panic.
861 // Body can't be also used for Opera which fills it with <br>
862 // what is indistinguishable from pasted <br> (copying <br> in Opera isn't possible,
863 // but it can be copied from other browser).
864 var pastebin = new CKEDITOR.dom.element(
865 ( CKEDITOR.env.webkit || editable.is( 'body' ) ) && !CKEDITOR.env.ie ? 'body' : 'div', doc );
866
867 pastebin.setAttributes( {
868 id: 'cke_pastebin',
869 'data-cke-temp': '1'
870 } );
871
872 var containerOffset = 0,
873 offsetParent,
874 win = doc.getWindow();
875
876 if ( CKEDITOR.env.webkit ) {
877 // It's better to paste close to the real paste destination, so inherited styles
878 // (which Webkits will try to compensate by styling span) differs less from the destination's one.
879 editable.append( pastebin );
880 // Style pastebin like .cke_editable, to minimize differences between origin and destination. (http://dev.ckeditor.com/ticket/9754)
881 pastebin.addClass( 'cke_editable' );
882
883 // Compensate position of offsetParent.
884 if ( !editable.is( 'body' ) ) {
885 // We're not able to get offsetParent from pastebin (body element), so check whether
886 // its parent (editable) is positioned.
887 if ( editable.getComputedStyle( 'position' ) != 'static' )
888 offsetParent = editable;
889 // And if not - safely get offsetParent from editable.
890 else
891 offsetParent = CKEDITOR.dom.element.get( editable.$.offsetParent );
892
893 containerOffset = offsetParent.getDocumentPosition().y;
894 }
895 } else {
896 // Opera and IE doesn't allow to append to html element.
897 editable.getAscendant( CKEDITOR.env.ie ? 'body' : 'html', 1 ).append( pastebin );
898 }
899
900 pastebin.setStyles( {
901 position: 'absolute',
902 // Position the bin at the top (+10 for safety) of viewport to avoid any subsequent document scroll.
903 top: ( win.getScrollPosition().y - containerOffset + 10 ) + 'px',
904 width: '1px',
905 // Caret has to fit in that height, otherwise browsers like Chrome & Opera will scroll window to show it.
906 // Set height equal to viewport's height - 20px (safety gaps), minimum 1px.
907 height: Math.max( 1, win.getViewPaneSize().height - 20 ) + 'px',
908 overflow: 'hidden',
909 // Reset styles that can mess up pastebin position.
910 margin: 0,
911 padding: 0
912 } );
913
914 // Paste fails in Safari when the body tag has 'user-select: none'. (http://dev.ckeditor.com/ticket/12506)
915 if ( CKEDITOR.env.safari )
916 pastebin.setStyles( CKEDITOR.tools.cssVendorPrefix( 'user-select', 'text' ) );
917
918 // Check if the paste bin now establishes new editing host.
919 var isEditingHost = pastebin.getParent().isReadOnly();
920
921 if ( isEditingHost ) {
922 // Hide the paste bin.
923 pastebin.setOpacity( 0 );
924 // And make it editable.
925 pastebin.setAttribute( 'contenteditable', true );
926 }
927 // Transparency is not enough since positioned non-editing host always shows
928 // resize handler, pull it off the screen instead.
929 else {
930 pastebin.setStyle( editor.config.contentsLangDirection == 'ltr' ? 'left' : 'right', '-10000px' );
931 }
932
933 editor.on( 'selectionChange', cancel, null, null, 0 );
934
935 // Webkit fill fire blur on editable when moving selection to
936 // pastebin (if body is used). Cancel it because it causes incorrect
937 // selection lock in case of inline editor (http://dev.ckeditor.com/ticket/10644).
938 // The same seems to apply to Firefox (http://dev.ckeditor.com/ticket/10787).
939 if ( CKEDITOR.env.webkit || CKEDITOR.env.gecko )
940 blurListener = editable.once( 'blur', cancel, null, null, -100 );
941
942 // Temporarily move selection to the pastebin.
943 isEditingHost && pastebin.focus();
944 var range = new CKEDITOR.dom.range( pastebin );
945 range.selectNodeContents( pastebin );
946 var selPastebin = range.select();
947
948 // If non-native paste is executed, IE will open security alert and blur editable.
949 // Editable will then lock selection inside itself and after accepting security alert
950 // this selection will be restored. We overwrite stored selection, so it's restored
951 // in pastebin. (http://dev.ckeditor.com/ticket/9552)
952 if ( CKEDITOR.env.ie ) {
953 blurListener = editable.once( 'blur', function() {
954 editor.lockSelection( selPastebin );
955 } );
956 }
957
958 var scrollTop = CKEDITOR.document.getWindow().getScrollPosition().y;
959
960 // Wait a while and grab the pasted contents.
961 setTimeout( function() {
962 // Restore main window's scroll position which could have been changed
963 // by browser in cases described in http://dev.ckeditor.com/ticket/9771.
964 if ( CKEDITOR.env.webkit )
965 CKEDITOR.document.getBody().$.scrollTop = scrollTop;
966
967 // Blur will be fired only on non-native paste. In other case manually remove listener.
968 blurListener && blurListener.removeListener();
969
970 // Restore properly the document focus. (http://dev.ckeditor.com/ticket/8849)
971 if ( CKEDITOR.env.ie )
972 editable.focus();
973
974 // IE7: selection must go before removing pastebin. (http://dev.ckeditor.com/ticket/8691)
975 sel.selectBookmarks( bms );
976 pastebin.remove();
977
978 // Grab the HTML contents.
979 // We need to look for a apple style wrapper on webkit it also adds
980 // a div wrapper if you copy/paste the body of the editor.
981 // Remove hidden div and restore selection.
982 var bogusSpan;
983 if ( CKEDITOR.env.webkit && ( bogusSpan = pastebin.getFirst() ) && ( bogusSpan.is && bogusSpan.hasClass( 'Apple-style-span' ) ) )
984 pastebin = bogusSpan;
985
986 editor.removeListener( 'selectionChange', cancel );
987 callback( pastebin.getHtml() );
988 }, 0 );
989 }
990
991 // Try to get content directly on IE from clipboard, without native event
992 // being fired before. In other words - synthetically get clipboard data, if it's possible.
993 // mainPasteEvent will be fired, so if forced native paste:
994 // * worked, getClipboardDataByPastebin will grab it,
995 // * didn't work, dataValue and dataTransfer will be empty and editor#paste won't be fired.
996 // Clipboard data can be accessed directly only on IEs older than Edge.
997 // On other browsers we should fire beforePaste event and return false.
998 function getClipboardDataDirectly() {
999 if ( clipboard.mainPasteEvent == 'paste' ) {
1000 editor.fire( 'beforePaste', { type: 'auto', method: 'paste' } );
1001 return false;
1002 }
1003
1004 // Prevent IE from pasting at the begining of the document.
1005 editor.focus();
1006
1007 // Command will be handled by 'beforepaste', but as
1008 // execIECommand( 'paste' ) will fire also 'paste' event
1009 // we're canceling it.
1010 preventPasteEventNow();
1011
1012 // http://dev.ckeditor.com/ticket/9247: Lock focus to prevent IE from hiding toolbar for inline editor.
1013 var focusManager = editor.focusManager;
1014 focusManager.lock();
1015
1016 if ( editor.editable().fire( clipboard.mainPasteEvent ) && !execIECommand( 'paste' ) ) {
1017 focusManager.unlock();
1018 return false;
1019 }
1020 focusManager.unlock();
1021
1022 return true;
1023 }
1024
1025 // Listens for some clipboard related keystrokes, so they get customized.
1026 // Needs to be bind to keydown event.
1027 function onKey( event ) {
1028 if ( editor.mode != 'wysiwyg' )
1029 return;
1030
1031 switch ( event.data.keyCode ) {
1032 // Paste
1033 case CKEDITOR.CTRL + 86: // CTRL+V
1034 case CKEDITOR.SHIFT + 45: // SHIFT+INS
1035 var editable = editor.editable();
1036
1037 // Cancel 'paste' event because ctrl+v is for IE handled
1038 // by 'beforepaste'.
1039 preventPasteEventNow();
1040
1041 // Simulate 'beforepaste' event for all browsers using 'paste' as main event.
1042 if ( clipboard.mainPasteEvent == 'paste' ) {
1043 editable.fire( 'beforepaste' );
1044 }
1045
1046 return;
1047
1048 // Cut
1049 case CKEDITOR.CTRL + 88: // CTRL+X
1050 case CKEDITOR.SHIFT + 46: // SHIFT+DEL
1051 // Save Undo snapshot.
1052 editor.fire( 'saveSnapshot' ); // Save before cut
1053 setTimeout( function() {
1054 editor.fire( 'saveSnapshot' ); // Save after cut
1055 }, 50 ); // OSX is slow (http://dev.ckeditor.com/ticket/11416).
1056 }
1057 }
1058
1059 function pasteDataFromClipboard( evt ) {
1060 // Default type is 'auto', but can be changed by beforePaste listeners.
1061 var eventData = {
1062 type: 'auto',
1063 method: 'paste',
1064 dataTransfer: clipboard.initPasteDataTransfer( evt )
1065 };
1066
1067 eventData.dataTransfer.cacheData();
1068
1069 // Fire 'beforePaste' event so clipboard flavor get customized by other plugins.
1070 // If 'beforePaste' is canceled continue executing getClipboardDataByPastebin and then do nothing
1071 // (do not fire 'paste', 'afterPaste' events). This way we can grab all - synthetically
1072 // and natively pasted content and prevent its insertion into editor
1073 // after canceling 'beforePaste' event.
1074 var beforePasteNotCanceled = editor.fire( 'beforePaste', eventData ) !== false;
1075
1076 // Do not use paste bin if the browser let us get HTML or files from dataTranfer.
1077 if ( beforePasteNotCanceled && clipboard.canClipboardApiBeTrusted( eventData.dataTransfer, editor ) ) {
1078 evt.data.preventDefault();
1079 setTimeout( function() {
1080 firePasteEvents( editor, eventData );
1081 }, 0 );
1082 } else {
1083 getClipboardDataByPastebin( evt, function( data ) {
1084 // Clean up.
1085 eventData.dataValue = data.replace( /<span[^>]+data-cke-bookmark[^<]*?<\/span>/ig, '' );
1086
1087 // Fire remaining events (without beforePaste)
1088 beforePasteNotCanceled && firePasteEvents( editor, eventData );
1089 } );
1090 }
1091 }
1092
1093 function setToolbarStates() {
1094 if ( editor.mode != 'wysiwyg' )
1095 return;
1096
1097 var pasteState = stateFromNamedCommand( 'paste' );
1098
1099 editor.getCommand( 'cut' ).setState( stateFromNamedCommand( 'cut' ) );
1100 editor.getCommand( 'copy' ).setState( stateFromNamedCommand( 'copy' ) );
1101 editor.getCommand( 'paste' ).setState( pasteState );
1102 editor.fire( 'pasteState', pasteState );
1103 }
1104
1105 function stateFromNamedCommand( command ) {
1106 if ( inReadOnly && command in { paste: 1, cut: 1 } )
1107 return CKEDITOR.TRISTATE_DISABLED;
1108
1109 if ( command == 'paste' )
1110 return CKEDITOR.TRISTATE_OFF;
1111
1112 // Cut, copy - check if the selection is not empty.
1113 var sel = editor.getSelection(),
1114 ranges = sel.getRanges(),
1115 selectionIsEmpty = sel.getType() == CKEDITOR.SELECTION_NONE || ( ranges.length == 1 && ranges[ 0 ].collapsed );
1116
1117 return selectionIsEmpty ? CKEDITOR.TRISTATE_DISABLED : CKEDITOR.TRISTATE_OFF;
1118 }
1119 }
1120
1121 // Returns:
1122 // * 'htmlifiedtext' if content looks like transformed by browser from plain text.
1123 // See clipboard/paste.html TCs for more info.
1124 // * 'html' if it is not 'htmlifiedtext'.
1125 function recogniseContentType( data ) {
1126 if ( CKEDITOR.env.webkit ) {
1127 // Plain text or ( <div><br></div> and text inside <div> ).
1128 if ( !data.match( /^[^<]*$/g ) && !data.match( /^(<div><br( ?\/)?><\/div>|<div>[^<]*<\/div>)*$/gi ) )
1129 return 'html';
1130 } else if ( CKEDITOR.env.ie ) {
1131 // Text and <br> or ( text and <br> in <p> - paragraphs can be separated by new \r\n ).
1132 if ( !data.match( /^([^<]|<br( ?\/)?>)*$/gi ) && !data.match( /^(<p>([^<]|<br( ?\/)?>)*<\/p>|(\r\n))*$/gi ) )
1133 return 'html';
1134 } else if ( CKEDITOR.env.gecko ) {
1135 // Text or <br>.
1136 if ( !data.match( /^([^<]|<br( ?\/)?>)*$/gi ) )
1137 return 'html';
1138 } else {
1139 return 'html';
1140 }
1141
1142 return 'htmlifiedtext';
1143 }
1144
1145 // This function transforms what browsers produce when
1146 // pasting plain text into editable element (see clipboard/paste.html TCs
1147 // for more info) into correct HTML (similar to that produced by text2Html).
1148 function htmlifiedTextHtmlification( config, data ) {
1149 function repeatParagraphs( repeats ) {
1150 // Repeat blocks floor((n+1)/2) times.
1151 // Even number of repeats - add <br> at the beginning of last <p>.
1152 return CKEDITOR.tools.repeat( '</p><p>', ~~( repeats / 2 ) ) + ( repeats % 2 == 1 ? '<br>' : '' );
1153 }
1154
1155 // Replace adjacent white-spaces (EOLs too - Fx sometimes keeps them) with one space.
1156 data = data.replace( /\s+/g, ' ' )
1157 // Remove spaces from between tags.
1158 .replace( /> +</g, '><' )
1159 // Normalize XHTML syntax and upper cased <br> tags.
1160 .replace( /<br ?\/>/gi, '<br>' );
1161
1162 // IE - lower cased tags.
1163 data = data.replace( /<\/?[A-Z]+>/g, function( match ) {
1164 return match.toLowerCase();
1165 } );
1166
1167 // Don't touch single lines (no <br|p|div>) - nothing to do here.
1168 if ( data.match( /^[^<]$/ ) )
1169 return data;
1170
1171 // Webkit.
1172 if ( CKEDITOR.env.webkit && data.indexOf( '<div>' ) > -1 ) {
1173 // One line break at the beginning - insert <br>
1174 data = data.replace( /^(<div>(<br>|)<\/div>)(?!$|(<div>(<br>|)<\/div>))/g, '<br>' )
1175 // Two or more - reduce number of new lines by one.
1176 .replace( /^(<div>(<br>|)<\/div>){2}(?!$)/g, '<div></div>' );
1177
1178 // Two line breaks create one paragraph in Webkit.
1179 if ( data.match( /<div>(<br>|)<\/div>/ ) ) {
1180 data = '<p>' + data.replace( /(<div>(<br>|)<\/div>)+/g, function( match ) {
1181 return repeatParagraphs( match.split( '</div><div>' ).length + 1 );
1182 } ) + '</p>';
1183 }
1184
1185 // One line break create br.
1186 data = data.replace( /<\/div><div>/g, '<br>' );
1187
1188 // Remove remaining divs.
1189 data = data.replace( /<\/?div>/g, '' );
1190 }
1191
1192 // Opera and Firefox and enterMode != BR.
1193 if ( CKEDITOR.env.gecko && config.enterMode != CKEDITOR.ENTER_BR ) {
1194 // Remove bogus <br> - Fx generates two <brs> for one line break.
1195 // For two line breaks it still produces two <brs>, but it's better to ignore this case than the first one.
1196 if ( CKEDITOR.env.gecko )
1197 data = data.replace( /^<br><br>$/, '<br>' );
1198
1199 // This line satisfy edge case when for Opera we have two line breaks
1200 //data = data.replace( /)
1201
1202 if ( data.indexOf( '<br><br>' ) > -1 ) {
1203 // Two line breaks create one paragraph, three - 2, four - 3, etc.
1204 data = '<p>' + data.replace( /(<br>){2,}/g, function( match ) {
1205 return repeatParagraphs( match.length / 4 );
1206 } ) + '</p>';
1207 }
1208 }
1209
1210 return switchEnterMode( config, data );
1211 }
1212
1213 function filtersFactoryFactory() {
1214 var filters = {};
1215
1216 function setUpTags() {
1217 var tags = {};
1218
1219 for ( var tag in CKEDITOR.dtd ) {
1220 if ( tag.charAt( 0 ) != '$' && tag != 'div' && tag != 'span' ) {
1221 tags[ tag ] = 1;
1222 }
1223 }
1224
1225 return tags;
1226 }
1227
1228 function createSemanticContentFilter() {
1229 var filter = new CKEDITOR.filter();
1230
1231 filter.allow( {
1232 $1: {
1233 elements: setUpTags(),
1234 attributes: true,
1235 styles: false,
1236 classes: false
1237 }
1238 } );
1239
1240 return filter;
1241 }
1242
1243 return {
1244 get: function( type ) {
1245 if ( type == 'plain-text' ) {
1246 // Does this look confusing to you? Did we forget about enter mode?
1247 // It is a trick that let's us creating one filter for edidtor, regardless of its
1248 // activeEnterMode (which as the name indicates can change during runtime).
1249 //
1250 // How does it work?
1251 // The active enter mode is passed to the filter.applyTo method.
1252 // The filter first marks all elements except <br> as disallowed and then tries to remove
1253 // them. However, it cannot remove e.g. a <p> element completely, because it's a basic structural element,
1254 // so it tries to replace it with an element created based on the active enter mode, eventually doing nothing.
1255 //
1256 // Now you can sleep well.
1257 return filters.plainText || ( filters.plainText = new CKEDITOR.filter( 'br' ) );
1258 } else if ( type == 'semantic-content' ) {
1259 return filters.semanticContent || ( filters.semanticContent = createSemanticContentFilter() );
1260 } else if ( type ) {
1261 // Create filter based on rules (string or object).
1262 return new CKEDITOR.filter( type );
1263 }
1264
1265 return null;
1266 }
1267 };
1268 }
1269
1270 function filterContent( editor, data, filter ) {
1271 var fragment = CKEDITOR.htmlParser.fragment.fromHtml( data ),
1272 writer = new CKEDITOR.htmlParser.basicWriter();
1273
1274 filter.applyTo( fragment, true, false, editor.activeEnterMode );
1275 fragment.writeHtml( writer );
1276
1277 return writer.getHtml();
1278 }
1279
1280 function switchEnterMode( config, data ) {
1281 if ( config.enterMode == CKEDITOR.ENTER_BR ) {
1282 data = data.replace( /(<\/p><p>)+/g, function( match ) {
1283 return CKEDITOR.tools.repeat( '<br>', match.length / 7 * 2 );
1284 } ).replace( /<\/?p>/g, '' );
1285 } else if ( config.enterMode == CKEDITOR.ENTER_DIV ) {
1286 data = data.replace( /<(\/)?p>/g, '<$1div>' );
1287 }
1288
1289 return data;
1290 }
1291
1292 function preventDefaultSetDropEffectToNone( evt ) {
1293 evt.data.preventDefault();
1294 evt.data.$.dataTransfer.dropEffect = 'none';
1295 }
1296
1297 function initDragDrop( editor ) {
1298 var clipboard = CKEDITOR.plugins.clipboard;
1299
1300 editor.on( 'contentDom', function() {
1301 var editable = editor.editable(),
1302 dropTarget = CKEDITOR.plugins.clipboard.getDropTarget( editor ),
1303 top = editor.ui.space( 'top' ),
1304 bottom = editor.ui.space( 'bottom' );
1305
1306 // -------------- DRAGOVER TOP & BOTTOM --------------
1307
1308 // Not allowing dragging on toolbar and bottom (http://dev.ckeditor.com/ticket/12613).
1309 clipboard.preventDefaultDropOnElement( top );
1310 clipboard.preventDefaultDropOnElement( bottom );
1311
1312 // -------------- DRAGSTART --------------
1313 // Listed on dragstart to mark internal and cross-editor drag & drop
1314 // and save range and selected HTML.
1315
1316 editable.attachListener( dropTarget, 'dragstart', fireDragEvent );
1317
1318 // Make sure to reset data transfer (in case dragend was not called or was canceled).
1319 editable.attachListener( editor, 'dragstart', clipboard.resetDragDataTransfer, clipboard, null, 1 );
1320
1321 // Create a dataTransfer object and save it globally.
1322 editable.attachListener( editor, 'dragstart', function( evt ) {
1323 clipboard.initDragDataTransfer( evt, editor );
1324 }, null, null, 2 );
1325
1326 editable.attachListener( editor, 'dragstart', function() {
1327 // Save drag range globally for cross editor D&D.
1328 var dragRange = clipboard.dragRange = editor.getSelection().getRanges()[ 0 ];
1329
1330 // Store number of children, so we can later tell if any text node was split on drop. (http://dev.ckeditor.com/ticket/13011, http://dev.ckeditor.com/ticket/13447)
1331 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 10 ) {
1332 clipboard.dragStartContainerChildCount = dragRange ? getContainerChildCount( dragRange.startContainer ) : null;
1333 clipboard.dragEndContainerChildCount = dragRange ? getContainerChildCount( dragRange.endContainer ) : null;
1334 }
1335 }, null, null, 100 );
1336
1337 // -------------- DRAGEND --------------
1338 // Clean up on dragend.
1339
1340 editable.attachListener( dropTarget, 'dragend', fireDragEvent );
1341
1342 // Init data transfer if someone wants to use it in dragend.
1343 editable.attachListener( editor, 'dragend', clipboard.initDragDataTransfer, clipboard, null, 1 );
1344
1345 // When drag & drop is done we need to reset dataTransfer so the future
1346 // external drop will be not recognize as internal.
1347 editable.attachListener( editor, 'dragend', clipboard.resetDragDataTransfer, clipboard, null, 100 );
1348
1349 // -------------- DRAGOVER --------------
1350 // We need to call preventDefault on dragover because otherwise if
1351 // we drop image it will overwrite document.
1352
1353 editable.attachListener( dropTarget, 'dragover', function( evt ) {
1354 // Edge requires this handler to have `preventDefault()` regardless of the situation.
1355 if ( CKEDITOR.env.edge ) {
1356 evt.data.preventDefault();
1357 return;
1358 }
1359
1360 var target = evt.data.getTarget();
1361
1362 // Prevent reloading page when dragging image on empty document (http://dev.ckeditor.com/ticket/12619).
1363 if ( target && target.is && target.is( 'html' ) ) {
1364 evt.data.preventDefault();
1365 return;
1366 }
1367
1368 // If we do not prevent default dragover on IE the file path
1369 // will be loaded and we will lose content. On the other hand
1370 // if we prevent it the cursor will not we shown, so we prevent
1371 // dragover only on IE, on versions which support file API and only
1372 // if the event contains files.
1373 if ( CKEDITOR.env.ie &&
1374 CKEDITOR.plugins.clipboard.isFileApiSupported &&
1375 evt.data.$.dataTransfer.types.contains( 'Files' ) ) {
1376 evt.data.preventDefault();
1377 }
1378 } );
1379
1380 // -------------- DROP --------------
1381
1382 editable.attachListener( dropTarget, 'drop', function( evt ) {
1383 // Do nothing if event was already prevented. (http://dev.ckeditor.com/ticket/13879)
1384 if ( evt.data.$.defaultPrevented ) {
1385 return;
1386 }
1387
1388 // Cancel native drop.
1389 evt.data.preventDefault();
1390
1391 var target = evt.data.getTarget(),
1392 readOnly = target.isReadOnly();
1393
1394 // Do nothing if drop on non editable element (http://dev.ckeditor.com/ticket/13015).
1395 // The <html> tag isn't editable (body is), but we want to allow drop on it
1396 // (so it is possible to drop below editor contents).
1397 if ( readOnly && !( target.type == CKEDITOR.NODE_ELEMENT && target.is( 'html' ) ) ) {
1398 return;
1399 }
1400
1401 // Getting drop position is one of the most complex parts.
1402 var dropRange = clipboard.getRangeAtDropPosition( evt, editor ),
1403 dragRange = clipboard.dragRange;
1404
1405 // Do nothing if it was not possible to get drop range.
1406 if ( !dropRange ) {
1407 return;
1408 }
1409
1410 // Fire drop.
1411 fireDragEvent( evt, dragRange, dropRange );
1412 }, null, null, 9999 );
1413
1414 // Create dataTransfer or get it, if it was created before.
1415 editable.attachListener( editor, 'drop', clipboard.initDragDataTransfer, clipboard, null, 1 );
1416
1417 // Execute drop action, fire paste.
1418 editable.attachListener( editor, 'drop', function( evt ) {
1419 var data = evt.data;
1420
1421 if ( !data ) {
1422 return;
1423 }
1424
1425 // Let user modify drag and drop range.
1426 var dropRange = data.dropRange,
1427 dragRange = data.dragRange,
1428 dataTransfer = data.dataTransfer;
1429
1430 if ( dataTransfer.getTransferType( editor ) == CKEDITOR.DATA_TRANSFER_INTERNAL ) {
1431 // Execute drop with a timeout because otherwise selection, after drop,
1432 // on IE is in the drag position, instead of drop position.
1433 setTimeout( function() {
1434 clipboard.internalDrop( dragRange, dropRange, dataTransfer, editor );
1435 }, 0 );
1436 } else if ( dataTransfer.getTransferType( editor ) == CKEDITOR.DATA_TRANSFER_CROSS_EDITORS ) {
1437 crossEditorDrop( dragRange, dropRange, dataTransfer );
1438 } else {
1439 externalDrop( dropRange, dataTransfer );
1440 }
1441 }, null, null, 9999 );
1442
1443 // Cross editor drag and drop (drag in one Editor and drop in the other).
1444 function crossEditorDrop( dragRange, dropRange, dataTransfer ) {
1445 // Paste event should be fired before delete contents because otherwise
1446 // Chrome have a problem with drop range (Chrome split the drop
1447 // range container so the offset is bigger then container length).
1448 dropRange.select();
1449 firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop' }, 1 );
1450
1451 // Remove dragged content and make a snapshot.
1452 dataTransfer.sourceEditor.fire( 'saveSnapshot' );
1453
1454 dataTransfer.sourceEditor.editable().extractHtmlFromRange( dragRange );
1455
1456 // Make some selection before saving snapshot, otherwise error will be thrown, because
1457 // there will be no valid selection after content is removed.
1458 dataTransfer.sourceEditor.getSelection().selectRanges( [ dragRange ] );
1459 dataTransfer.sourceEditor.fire( 'saveSnapshot' );
1460 }
1461
1462 // Drop from external source.
1463 function externalDrop( dropRange, dataTransfer ) {
1464 // Paste content into the drop position.
1465 dropRange.select();
1466
1467 firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop' }, 1 );
1468
1469 // Usually we reset DataTranfer on dragend,
1470 // but dragend is called on the same element as dragstart
1471 // so it will not be called on on external drop.
1472 clipboard.resetDragDataTransfer();
1473 }
1474
1475 // Fire drag/drop events (dragstart, dragend, drop).
1476 function fireDragEvent( evt, dragRange, dropRange ) {
1477 var eventData = {
1478 $: evt.data.$,
1479 target: evt.data.getTarget()
1480 };
1481
1482 if ( dragRange ) {
1483 eventData.dragRange = dragRange;
1484 }
1485 if ( dropRange ) {
1486 eventData.dropRange = dropRange;
1487 }
1488
1489 if ( editor.fire( evt.name, eventData ) === false ) {
1490 evt.data.preventDefault();
1491 }
1492 }
1493
1494 function getContainerChildCount( container ) {
1495 if ( container.type != CKEDITOR.NODE_ELEMENT ) {
1496 container = container.getParent();
1497 }
1498
1499 return container.getChildCount();
1500 }
1501 } );
1502 }
1503
1504 /**
1505 * @singleton
1506 * @class CKEDITOR.plugins.clipboard
1507 */
1508 CKEDITOR.plugins.clipboard = {
1509 /**
1510 * True if the environment allows to set data on copy or cut manually. This value is false in IE, because this browser
1511 * shows the security dialog window when the script tries to set clipboard data and on iOS, because custom data is
1512 * not saved to clipboard there.
1513 *
1514 * @since 4.5
1515 * @readonly
1516 * @property {Boolean}
1517 */
1518 isCustomCopyCutSupported: !CKEDITOR.env.ie && !CKEDITOR.env.iOS,
1519
1520 /**
1521 * True if the environment supports MIME types and custom data types in dataTransfer/cliboardData getData/setData methods.
1522 *
1523 * @since 4.5
1524 * @readonly
1525 * @property {Boolean}
1526 */
1527 isCustomDataTypesSupported: !CKEDITOR.env.ie,
1528
1529 /**
1530 * True if the environment supports File API.
1531 *
1532 * @since 4.5
1533 * @readonly
1534 * @property {Boolean}
1535 */
1536 isFileApiSupported: !CKEDITOR.env.ie || CKEDITOR.env.version > 9,
1537
1538 /**
1539 * Main native paste event editable should listen to.
1540 *
1541 * **Note:** Safari does not like the {@link CKEDITOR.editor#beforePaste} event &mdash; it sometimes does not
1542 * handle <kbd>Ctrl+C</kbd> properly. This is probably caused by some race condition between events.
1543 * Chrome, Firefox and Edge work well with both events, so it is better to use {@link CKEDITOR.editor#paste}
1544 * which will handle pasting from e.g. browsers' menu bars.
1545 * IE7/8 does not like the {@link CKEDITOR.editor#paste} event for which it is throwing random errors.
1546 *
1547 * @since 4.5
1548 * @readonly
1549 * @property {String}
1550 */
1551 mainPasteEvent: ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) ? 'beforepaste' : 'paste',
1552
1553 /**
1554 * Returns `true` if it is expected that a browser provides HTML data through the Clipboard API.
1555 * If not, this method returns `false` and as a result CKEditor will use the paste bin. Read more in
1556 * the [Clipboard Integration](http://docs.ckeditor.com/#!/guide/dev_clipboard-section-clipboard-api) guide.
1557 *
1558 * @since 4.5.2
1559 * @returns {Boolean}
1560 */
1561 canClipboardApiBeTrusted: function( dataTransfer, editor ) {
1562 // If it's an internal or cross-editor data transfer, then it means that custom cut/copy/paste support works
1563 // and that the data were put manually on the data transfer so we can be sure that it's available.
1564 if ( dataTransfer.getTransferType( editor ) != CKEDITOR.DATA_TRANSFER_EXTERNAL ) {
1565 return true;
1566 }
1567
1568 // In Chrome we can trust Clipboard API, with the exception of Chrome on Android (in both - mobile and desktop modes), where
1569 // clipboard API is not available so we need to check it (http://dev.ckeditor.com/ticket/13187).
1570 if ( CKEDITOR.env.chrome && !dataTransfer.isEmpty() ) {
1571 return true;
1572 }
1573
1574 // Because of a Firefox bug HTML data are not available in some cases (e.g. paste from Word), in such cases we
1575 // need to use the pastebin (http://dev.ckeditor.com/ticket/13528, https://bugzilla.mozilla.org/show_bug.cgi?id=1183686).
1576 if ( CKEDITOR.env.gecko && ( dataTransfer.getData( 'text/html' ) || dataTransfer.getFilesCount() ) ) {
1577 return true;
1578 }
1579
1580 // Safari fixed clipboard in 10.1 (https://bugs.webkit.org/show_bug.cgi?id=19893) (http://dev.ckeditor.com/ticket/16982).
1581 // However iOS version still doesn't work well enough (https://bugs.webkit.org/show_bug.cgi?id=19893#c34).
1582 if ( CKEDITOR.env.safari && CKEDITOR.env.version >= 603 && !CKEDITOR.env.iOS ) {
1583 return true;
1584 }
1585
1586 // In older Safari and IE HTML data is not available though the Clipboard API.
1587 // In Edge things are a bit messy at the moment -
1588 // https://connect.microsoft.com/IE/feedback/details/1572456/edge-clipboard-api-text-html-content-messed-up-in-event-clipboarddata
1589 // It is safer to use the paste bin in unknown cases.
1590 return false;
1591 },
1592
1593 /**
1594 * Returns the element that should be used as the target for the drop event.
1595 *
1596 * @since 4.5
1597 * @param {CKEDITOR.editor} editor The editor instance.
1598 * @returns {CKEDITOR.dom.domObject} the element that should be used as the target for the drop event.
1599 */
1600 getDropTarget: function( editor ) {
1601 var editable = editor.editable();
1602
1603 // http://dev.ckeditor.com/ticket/11123 Firefox needs to listen on document, because otherwise event won't be fired.
1604 // http://dev.ckeditor.com/ticket/11086 IE8 cannot listen on document.
1605 if ( ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) || editable.isInline() ) {
1606 return editable;
1607 } else {
1608 return editor.document;
1609 }
1610 },
1611
1612 /**
1613 * IE 8 & 9 split text node on drop so the first node contains the
1614 * text before the drop position and the second contains the rest. If you
1615 * drag the content from the same node you will be not be able to get
1616 * it (the range becomes invalid), so you need to join them back.
1617 *
1618 * Note that the first node in IE 8 & 9 is the original node object
1619 * but with shortened content.
1620 *
1621 * Before:
1622 * --- Text Node A ----------------------------------
1623 * /\
1624 * Drag position
1625 *
1626 * After (IE 8 & 9):
1627 * --- Text Node A ----- --- Text Node B -----------
1628 * /\ /\
1629 * Drop position Drag position
1630 * (invalid)
1631 *
1632 * After (other browsers):
1633 * --- Text Node A ----------------------------------
1634 * /\ /\
1635 * Drop position Drag position
1636 *
1637 * **Note:** This function is in the public scope for tests usage only.
1638 *
1639 * @since 4.5
1640 * @private
1641 * @param {CKEDITOR.dom.range} dragRange The drag range.
1642 * @param {CKEDITOR.dom.range} dropRange The drop range.
1643 * @param {Number} preDragStartContainerChildCount The number of children of the drag range start container before the drop.
1644 * @param {Number} preDragEndContainerChildCount The number of children of the drag range end container before the drop.
1645 */
1646 fixSplitNodesAfterDrop: function( dragRange, dropRange, preDragStartContainerChildCount, preDragEndContainerChildCount ) {
1647 var dropContainer = dropRange.startContainer;
1648
1649 if (
1650 typeof preDragEndContainerChildCount != 'number' ||
1651 typeof preDragStartContainerChildCount != 'number'
1652 ) {
1653 return;
1654 }
1655
1656 // We are only concerned about ranges anchored in elements.
1657 if ( dropContainer.type != CKEDITOR.NODE_ELEMENT ) {
1658 return;
1659 }
1660
1661 if ( handleContainer( dragRange.startContainer, dropContainer, preDragStartContainerChildCount ) ) {
1662 return;
1663 }
1664
1665 if ( handleContainer( dragRange.endContainer, dropContainer, preDragEndContainerChildCount ) ) {
1666 return;
1667 }
1668
1669 function handleContainer( dragContainer, dropContainer, preChildCount ) {
1670 var dragElement = dragContainer;
1671 if ( dragElement.type == CKEDITOR.NODE_TEXT ) {
1672 dragElement = dragContainer.getParent();
1673 }
1674
1675 if ( dragElement.equals( dropContainer ) && preChildCount != dropContainer.getChildCount() ) {
1676 applyFix( dropRange );
1677 return true;
1678 }
1679 }
1680
1681 function applyFix( dropRange ) {
1682 var nodeBefore = dropRange.startContainer.getChild( dropRange.startOffset - 1 ),
1683 nodeAfter = dropRange.startContainer.getChild( dropRange.startOffset );
1684
1685 if (
1686 nodeBefore && nodeBefore.type == CKEDITOR.NODE_TEXT &&
1687 nodeAfter && nodeAfter.type == CKEDITOR.NODE_TEXT
1688 ) {
1689 var offset = nodeBefore.getLength();
1690
1691 nodeBefore.setText( nodeBefore.getText() + nodeAfter.getText() );
1692 nodeAfter.remove();
1693
1694 dropRange.setStart( nodeBefore, offset );
1695 dropRange.collapse( true );
1696 }
1697 }
1698 },
1699
1700 /**
1701 * Checks whether turning the drag range into bookmarks will invalidate the drop range.
1702 * This usually happens when the drop range shares the container with the drag range and is
1703 * located after the drag range, but there are countless edge cases.
1704 *
1705 * This function is stricly related to {@link #internalDrop} which toggles
1706 * order in which it creates bookmarks for both ranges based on a value returned
1707 * by this method. In some cases this method returns a value which is not necessarily
1708 * true in terms of what it was meant to check, but it is convenient, because
1709 * we know how it is interpreted in {@link #internalDrop}, so the correct
1710 * behavior of the entire algorithm is assured.
1711 *
1712 * **Note:** This function is in the public scope for tests usage only.
1713 *
1714 * @since 4.5
1715 * @private
1716 * @param {CKEDITOR.dom.range} dragRange The first range to compare.
1717 * @param {CKEDITOR.dom.range} dropRange The second range to compare.
1718 * @returns {Boolean} `true` if the first range is before the second range.
1719 */
1720 isDropRangeAffectedByDragRange: function( dragRange, dropRange ) {
1721 var dropContainer = dropRange.startContainer,
1722 dropOffset = dropRange.endOffset;
1723
1724 // Both containers are the same and drop offset is at the same position or later.
1725 // " A L] A " " M A "
1726 // ^ ^
1727 if ( dragRange.endContainer.equals( dropContainer ) && dragRange.endOffset <= dropOffset ) {
1728 return true;
1729 }
1730
1731 // Bookmark for drag start container will mess up with offsets.
1732 // " O [L A " " M A "
1733 // ^ ^
1734 if (
1735 dragRange.startContainer.getParent().equals( dropContainer ) &&
1736 dragRange.startContainer.getIndex() < dropOffset
1737 ) {
1738 return true;
1739 }
1740
1741 // Bookmark for drag end container will mess up with offsets.
1742 // " O] L A " " M A "
1743 // ^ ^
1744 if (
1745 dragRange.endContainer.getParent().equals( dropContainer ) &&
1746 dragRange.endContainer.getIndex() < dropOffset
1747 ) {
1748 return true;
1749 }
1750
1751 return false;
1752 },
1753
1754 /**
1755 * Internal drag and drop (drag and drop in the same editor instance).
1756 *
1757 * **Note:** This function is in the public scope for tests usage only.
1758 *
1759 * @since 4.5
1760 * @private
1761 * @param {CKEDITOR.dom.range} dragRange The first range to compare.
1762 * @param {CKEDITOR.dom.range} dropRange The second range to compare.
1763 * @param {CKEDITOR.plugins.clipboard.dataTransfer} dataTransfer
1764 * @param {CKEDITOR.editor} editor
1765 */
1766 internalDrop: function( dragRange, dropRange, dataTransfer, editor ) {
1767 var clipboard = CKEDITOR.plugins.clipboard,
1768 editable = editor.editable(),
1769 dragBookmark, dropBookmark, isDropRangeAffected;
1770
1771 // Save and lock snapshot so there will be only
1772 // one snapshot for both remove and insert content.
1773 editor.fire( 'saveSnapshot' );
1774 editor.fire( 'lockSnapshot', { dontUpdate: 1 } );
1775
1776 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 10 ) {
1777 this.fixSplitNodesAfterDrop(
1778 dragRange,
1779 dropRange,
1780 clipboard.dragStartContainerChildCount,
1781 clipboard.dragEndContainerChildCount
1782 );
1783 }
1784
1785 // Because we manipulate multiple ranges we need to do it carefully,
1786 // changing one range (event creating a bookmark) may make other invalid.
1787 // We need to change ranges into bookmarks so we can manipulate them easily in the future.
1788 // We can change the range which is later in the text before we change the preceding range.
1789 // We call isDropRangeAffectedByDragRange to test the order of ranges.
1790 isDropRangeAffected = this.isDropRangeAffectedByDragRange( dragRange, dropRange );
1791 if ( !isDropRangeAffected ) {
1792 dragBookmark = dragRange.createBookmark( false );
1793 }
1794 dropBookmark = dropRange.clone().createBookmark( false );
1795 if ( isDropRangeAffected ) {
1796 dragBookmark = dragRange.createBookmark( false );
1797 }
1798
1799 // Check if drop range is inside range.
1800 // This is an edge case when we drop something on editable's margin/padding.
1801 // That space is not treated as a part of the range we drag, so it is possible to drop there.
1802 // When we drop, browser tries to find closest drop position and it finds it inside drag range. (http://dev.ckeditor.com/ticket/13453)
1803 var startNode = dragBookmark.startNode,
1804 endNode = dragBookmark.endNode,
1805 dropNode = dropBookmark.startNode,
1806 dropInsideDragRange =
1807 // Must check endNode because dragRange could be collapsed in some edge cases (simulated DnD).
1808 endNode &&
1809 ( startNode.getPosition( dropNode ) & CKEDITOR.POSITION_PRECEDING ) &&
1810 ( endNode.getPosition( dropNode ) & CKEDITOR.POSITION_FOLLOWING );
1811
1812 // If the drop range happens to be inside drag range change it's position to the beginning of the drag range.
1813 if ( dropInsideDragRange ) {
1814 // We only change position of bookmark span that is connected with dropBookmark.
1815 // dropRange will be overwritten and set to the dropBookmark later.
1816 dropNode.insertBefore( startNode );
1817 }
1818
1819 // No we can safely delete content for the drag range...
1820 dragRange = editor.createRange();
1821 dragRange.moveToBookmark( dragBookmark );
1822 editable.extractHtmlFromRange( dragRange, 1 );
1823
1824 // ...and paste content into the drop position.
1825 dropRange = editor.createRange();
1826 dropRange.moveToBookmark( dropBookmark );
1827
1828 // We do not select drop range, because of may be in the place we can not set the selection
1829 // (e.g. between blocks, in case of block widget D&D). We put range to the paste event instead.
1830 firePasteEvents( editor, { dataTransfer: dataTransfer, method: 'drop', range: dropRange }, 1 );
1831
1832 editor.fire( 'unlockSnapshot' );
1833 },
1834
1835 /**
1836 * Gets the range from the `drop` event.
1837 *
1838 * @since 4.5
1839 * @param {Object} domEvent A native DOM drop event object.
1840 * @param {CKEDITOR.editor} editor The source editor instance.
1841 * @returns {CKEDITOR.dom.range} range at drop position.
1842 */
1843 getRangeAtDropPosition: function( dropEvt, editor ) {
1844 var $evt = dropEvt.data.$,
1845 x = $evt.clientX,
1846 y = $evt.clientY,
1847 $range,
1848 defaultRange = editor.getSelection( true ).getRanges()[ 0 ],
1849 range = editor.createRange();
1850
1851 // Make testing possible.
1852 if ( dropEvt.data.testRange )
1853 return dropEvt.data.testRange;
1854
1855 // Webkits.
1856 if ( document.caretRangeFromPoint && editor.document.$.caretRangeFromPoint( x, y ) ) {
1857 $range = editor.document.$.caretRangeFromPoint( x, y );
1858 range.setStart( CKEDITOR.dom.node( $range.startContainer ), $range.startOffset );
1859 range.collapse( true );
1860 }
1861 // FF.
1862 else if ( $evt.rangeParent ) {
1863 range.setStart( CKEDITOR.dom.node( $evt.rangeParent ), $evt.rangeOffset );
1864 range.collapse( true );
1865 }
1866 // IEs 9+.
1867 // We check if editable is focused to make sure that it's an internal DnD. External DnD must use the second
1868 // mechanism because of http://dev.ckeditor.com/ticket/13472#comment:6.
1869 else if ( CKEDITOR.env.ie && CKEDITOR.env.version > 8 && defaultRange && editor.editable().hasFocus ) {
1870 // On IE 9+ range by default is where we expected it.
1871 // defaultRange may be undefined if dragover was canceled (file drop).
1872 return defaultRange;
1873 }
1874 // IE 8 and all IEs if !defaultRange or external DnD.
1875 else if ( document.body.createTextRange ) {
1876 // To use this method we need a focus (which may be somewhere else in case of external drop).
1877 editor.focus();
1878
1879 $range = editor.document.getBody().$.createTextRange();
1880 try {
1881 var sucess = false;
1882
1883 // If user drop between text line IEs moveToPoint throws exception:
1884 //
1885 // Lorem ipsum pulvinar purus et euismod
1886 //
1887 // dolor sit amet,| consectetur adipiscing
1888 // *
1889 // vestibulum tincidunt augue eget tempus.
1890 //
1891 // * - drop position
1892 // | - expected cursor position
1893 //
1894 // So we try to call moveToPoint with +-1px up to +-20px above or
1895 // below original drop position to find nearest good drop position.
1896 for ( var i = 0; i < 20 && !sucess; i++ ) {
1897 if ( !sucess ) {
1898 try {
1899 $range.moveToPoint( x, y - i );
1900 sucess = true;
1901 } catch ( err ) {
1902 }
1903 }
1904 if ( !sucess ) {
1905 try {
1906 $range.moveToPoint( x, y + i );
1907 sucess = true;
1908 } catch ( err ) {
1909 }
1910 }
1911 }
1912
1913 if ( sucess ) {
1914 var id = 'cke-temp-' + ( new Date() ).getTime();
1915 $range.pasteHTML( '<span id="' + id + '">\u200b</span>' );
1916
1917 var span = editor.document.getById( id );
1918 range.moveToPosition( span, CKEDITOR.POSITION_BEFORE_START );
1919 span.remove();
1920 } else {
1921 // If the fist method does not succeed we might be next to
1922 // the short element (like header):
1923 //
1924 // Lorem ipsum pulvinar purus et euismod.
1925 //
1926 //
1927 // SOME HEADER| *
1928 //
1929 //
1930 // vestibulum tincidunt augue eget tempus.
1931 //
1932 // * - drop position
1933 // | - expected cursor position
1934 //
1935 // In such situation elementFromPoint returns proper element. Using getClientRect
1936 // it is possible to check if the cursor should be at the beginning or at the end
1937 // of paragraph.
1938 var $element = editor.document.$.elementFromPoint( x, y ),
1939 element = new CKEDITOR.dom.element( $element ),
1940 rect;
1941
1942 if ( !element.equals( editor.editable() ) && element.getName() != 'html' ) {
1943 rect = element.getClientRect();
1944
1945 if ( x < rect.left ) {
1946 range.setStartAt( element, CKEDITOR.POSITION_AFTER_START );
1947 range.collapse( true );
1948 } else {
1949 range.setStartAt( element, CKEDITOR.POSITION_BEFORE_END );
1950 range.collapse( true );
1951 }
1952 }
1953 // If drop happens on no element elementFromPoint returns html or body.
1954 //
1955 // * |Lorem ipsum pulvinar purus et euismod.
1956 //
1957 // vestibulum tincidunt augue eget tempus.
1958 //
1959 // * - drop position
1960 // | - expected cursor position
1961 //
1962 // In such case we can try to use default selection. If startContainer is not
1963 // 'editable' element it is probably proper selection.
1964 else if ( defaultRange && defaultRange.startContainer &&
1965 !defaultRange.startContainer.equals( editor.editable() ) ) {
1966 return defaultRange;
1967
1968 // Otherwise we can not find any drop position and we have to return null
1969 // and cancel drop event.
1970 } else {
1971 return null;
1972 }
1973
1974 }
1975 } catch ( err ) {
1976 return null;
1977 }
1978 } else {
1979 return null;
1980 }
1981
1982 return range;
1983 },
1984
1985 /**
1986 * This function tries to link the `evt.data.dataTransfer` property of the {@link CKEDITOR.editor#dragstart},
1987 * {@link CKEDITOR.editor#dragend} and {@link CKEDITOR.editor#drop} events to a single
1988 * {@link CKEDITOR.plugins.clipboard.dataTransfer} object.
1989 *
1990 * This method is automatically used by the core of the drag and drop functionality and
1991 * usually does not have to be called manually when using the drag and drop events.
1992 *
1993 * This method behaves differently depending on whether the drag and drop events were fired
1994 * artificially (to represent a non-native drag and drop) or whether they were caused by the native drag and drop.
1995 *
1996 * If the native event is not available, then it will create a new {@link CKEDITOR.plugins.clipboard.dataTransfer}
1997 * instance (if it does not exist already) and will link it to this and all following event objects until
1998 * the {@link #resetDragDataTransfer} method is called. It means that all three drag and drop events must be fired
1999 * in order to ensure that the data transfer is bound correctly.
2000 *
2001 * If the native event is available, then the {@link CKEDITOR.plugins.clipboard.dataTransfer} is identified
2002 * by its ID and a new instance is assigned to the `evt.data.dataTransfer` only if the ID changed or
2003 * the {@link #resetDragDataTransfer} method was called.
2004 *
2005 * @since 4.5
2006 * @param {CKEDITOR.dom.event} [evt] A drop event object.
2007 * @param {CKEDITOR.editor} [sourceEditor] The source editor instance.
2008 */
2009 initDragDataTransfer: function( evt, sourceEditor ) {
2010 // Create a new dataTransfer object based on the drop event.
2011 // If this event was used on dragstart to create dataTransfer
2012 // both dataTransfer objects will have the same id.
2013 var nativeDataTransfer = evt.data.$ ? evt.data.$.dataTransfer : null,
2014 dataTransfer = new this.dataTransfer( nativeDataTransfer, sourceEditor );
2015
2016 if ( !nativeDataTransfer ) {
2017 // No native event.
2018 if ( this.dragData ) {
2019 dataTransfer = this.dragData;
2020 } else {
2021 this.dragData = dataTransfer;
2022 }
2023 } else {
2024 // Native event. If there is the same id we will replace dataTransfer with the one
2025 // created on drag, because it contains drag editor, drag content and so on.
2026 // Otherwise (in case of drag from external source) we save new object to
2027 // the global clipboard.dragData.
2028 if ( this.dragData && dataTransfer.id == this.dragData.id ) {
2029 dataTransfer = this.dragData;
2030 } else {
2031 this.dragData = dataTransfer;
2032 }
2033 }
2034
2035 evt.data.dataTransfer = dataTransfer;
2036 },
2037
2038 /**
2039 * Removes the global {@link #dragData} so the next call to {@link #initDragDataTransfer}
2040 * always creates a new instance of {@link CKEDITOR.plugins.clipboard.dataTransfer}.
2041 *
2042 * @since 4.5
2043 */
2044 resetDragDataTransfer: function() {
2045 this.dragData = null;
2046 },
2047
2048 /**
2049 * Global object storing the data transfer of the current drag and drop operation.
2050 * Do not use it directly, use {@link #initDragDataTransfer} and {@link #resetDragDataTransfer}.
2051 *
2052 * Note: This object is global (meaning that it is not related to a single editor instance)
2053 * in order to handle drag and drop from one editor into another.
2054 *
2055 * @since 4.5
2056 * @private
2057 * @property {CKEDITOR.plugins.clipboard.dataTransfer} dragData
2058 */
2059
2060 /**
2061 * Range object to save the drag range and remove its content after the drop.
2062 *
2063 * @since 4.5
2064 * @private
2065 * @property {CKEDITOR.dom.range} dragRange
2066 */
2067
2068 /**
2069 * Initializes and links data transfer objects based on the paste event. If the data
2070 * transfer object was already initialized on this event, the function will
2071 * return that object. In IE it is not possible to link copy/cut and paste events
2072 * so the method always returns a new object. The same happens if there is no paste event
2073 * passed to the method.
2074 *
2075 * @since 4.5
2076 * @param {CKEDITOR.dom.event} [evt] A paste event object.
2077 * @param {CKEDITOR.editor} [sourceEditor] The source editor instance.
2078 * @returns {CKEDITOR.plugins.clipboard.dataTransfer} The data transfer object.
2079 */
2080 initPasteDataTransfer: function( evt, sourceEditor ) {
2081 if ( !this.isCustomCopyCutSupported ) {
2082 // Edge does not support custom copy/cut, but it have some useful data in the clipboardData (http://dev.ckeditor.com/ticket/13755).
2083 return new this.dataTransfer( ( CKEDITOR.env.edge && evt && evt.data.$ && evt.data.$.clipboardData ) || null, sourceEditor );
2084 } else if ( evt && evt.data && evt.data.$ ) {
2085 var dataTransfer = new this.dataTransfer( evt.data.$.clipboardData, sourceEditor );
2086
2087 if ( this.copyCutData && dataTransfer.id == this.copyCutData.id ) {
2088 dataTransfer = this.copyCutData;
2089 dataTransfer.$ = evt.data.$.clipboardData;
2090 } else {
2091 this.copyCutData = dataTransfer;
2092 }
2093
2094 return dataTransfer;
2095 } else {
2096 return new this.dataTransfer( null, sourceEditor );
2097 }
2098 },
2099
2100 /**
2101 * Prevents dropping on the specified element.
2102 *
2103 * @since 4.5
2104 * @param {CKEDITOR.dom.element} element The element on which dropping should be disabled.
2105 */
2106 preventDefaultDropOnElement: function( element ) {
2107 element && element.on( 'dragover', preventDefaultSetDropEffectToNone );
2108 }
2109 };
2110
2111 // Data type used to link drag and drop events.
2112 //
2113 // In IE URL data type is buggie and there is no way to mark drag & drop without
2114 // modifying text data (which would be displayed if user drop content to the textarea)
2115 // so we just read dragged text.
2116 //
2117 // In Chrome and Firefox we can use custom data types.
2118 var clipboardIdDataType = CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ? 'cke/id' : 'Text';
2119 /**
2120 * Facade for the native `dataTransfer`/`clipboadData` object to hide all differences
2121 * between browsers.
2122 *
2123 * @since 4.5
2124 * @class CKEDITOR.plugins.clipboard.dataTransfer
2125 * @constructor Creates a class instance.
2126 * @param {Object} [nativeDataTransfer] A native data transfer object.
2127 * @param {CKEDITOR.editor} [editor] The source editor instance. If the editor is defined, dataValue will
2128 * be created based on the editor content and the type will be 'html'.
2129 */
2130 CKEDITOR.plugins.clipboard.dataTransfer = function( nativeDataTransfer, editor ) {
2131 if ( nativeDataTransfer ) {
2132 this.$ = nativeDataTransfer;
2133 }
2134
2135 this._ = {
2136 metaRegExp: /^<meta.*?>/i,
2137 bodyRegExp: /<body(?:[\s\S]*?)>([\s\S]*)<\/body>/i,
2138 fragmentRegExp: /<!--(?:Start|End)Fragment-->/g,
2139
2140 data: {},
2141 files: [],
2142
2143 normalizeType: function( type ) {
2144 type = type.toLowerCase();
2145
2146 if ( type == 'text' || type == 'text/plain' ) {
2147 return 'Text'; // IE support only Text and URL;
2148 } else if ( type == 'url' ) {
2149 return 'URL'; // IE support only Text and URL;
2150 } else {
2151 return type;
2152 }
2153 }
2154 };
2155
2156 // Check if ID is already created.
2157 this.id = this.getData( clipboardIdDataType );
2158
2159 // If there is no ID we need to create it. Different browsers needs different ID.
2160 if ( !this.id ) {
2161 if ( clipboardIdDataType == 'Text' ) {
2162 // For IE10+ only Text data type is supported and we have to compare dragged
2163 // and dropped text. If the ID is not set it means that empty string was dragged
2164 // (ex. image with no alt). We change null to empty string.
2165 this.id = '';
2166 } else {
2167 // String for custom data type.
2168 this.id = 'cke-' + CKEDITOR.tools.getUniqueId();
2169 }
2170 }
2171
2172 // In IE10+ we can not use any data type besides text, so we do not call setData.
2173 if ( clipboardIdDataType != 'Text' ) {
2174 // Try to set ID so it will be passed from the drag to the drop event.
2175 // On some browsers with some event it is not possible to setData so we
2176 // need to catch exceptions.
2177 try {
2178 this.$.setData( clipboardIdDataType, this.id );
2179 } catch ( err ) {}
2180 }
2181
2182 if ( editor ) {
2183 this.sourceEditor = editor;
2184
2185 this.setData( 'text/html', editor.getSelectedHtml( 1 ) );
2186
2187 // Without setData( 'text', ... ) on dragstart there is no drop event in Safari.
2188 // Also 'text' data is empty as drop to the textarea does not work if we do not put there text.
2189 if ( clipboardIdDataType != 'Text' && !this.getData( 'text/plain' ) ) {
2190 this.setData( 'text/plain', editor.getSelection().getSelectedText() );
2191 }
2192 }
2193
2194 /**
2195 * Data transfer ID used to bind all dataTransfer
2196 * objects based on the same event (e.g. in drag and drop events).
2197 *
2198 * @readonly
2199 * @property {String} id
2200 */
2201
2202 /**
2203 * A native DOM event object.
2204 *
2205 * @readonly
2206 * @property {Object} $
2207 */
2208
2209 /**
2210 * Source editor &mdash; the editor where the drag starts.
2211 * Might be undefined if the drag starts outside the editor (e.g. when dropping files to the editor).
2212 *
2213 * @readonly
2214 * @property {CKEDITOR.editor} sourceEditor
2215 */
2216
2217 /**
2218 * Private properties and methods.
2219 *
2220 * @private
2221 * @property {Object} _
2222 */
2223 };
2224
2225 /**
2226 * Data transfer operation (drag and drop or copy and paste) started and ended in the same
2227 * editor instance.
2228 *
2229 * @since 4.5
2230 * @readonly
2231 * @property {Number} [=1]
2232 * @member CKEDITOR
2233 */
2234 CKEDITOR.DATA_TRANSFER_INTERNAL = 1;
2235
2236 /**
2237 * Data transfer operation (drag and drop or copy and paste) started in one editor
2238 * instance and ended in another.
2239 *
2240 * @since 4.5
2241 * @readonly
2242 * @property {Number} [=2]
2243 * @member CKEDITOR
2244 */
2245 CKEDITOR.DATA_TRANSFER_CROSS_EDITORS = 2;
2246
2247 /**
2248 * Data transfer operation (drag and drop or copy and paste) started outside of the editor.
2249 * The source of the data may be a textarea, HTML, another application, etc.
2250 *
2251 * @since 4.5
2252 * @readonly
2253 * @property {Number} [=3]
2254 * @member CKEDITOR
2255 */
2256 CKEDITOR.DATA_TRANSFER_EXTERNAL = 3;
2257
2258 CKEDITOR.plugins.clipboard.dataTransfer.prototype = {
2259 /**
2260 * Facade for the native `getData` method.
2261 *
2262 * @param {String} type The type of data to retrieve.
2263 * @param {Boolean} [getNative=false] Indicates if the whole, original content of the dataTransfer should be returned.
2264 * Introduced in CKEditor 4.7.0.
2265 * @returns {String} type Stored data for the given type or an empty string if the data for that type does not exist.
2266 */
2267 getData: function( type, getNative ) {
2268 function isEmpty( data ) {
2269 return data === undefined || data === null || data === '';
2270 }
2271
2272 function filterUnwantedCharacters( data ) {
2273 if ( typeof data !== 'string' ) {
2274 return data;
2275 }
2276
2277 var htmlEnd = data.indexOf( '</html>' );
2278
2279 if ( htmlEnd !== -1 ) {
2280 // Just cut everything after `</html>`, so everything after htmlEnd index + length of `</html>`.
2281 // Required to workaround bug: https://bugs.chromium.org/p/chromium/issues/detail?id=696978
2282 return data.substring( 0, htmlEnd + 7 );
2283 }
2284
2285 return data;
2286 }
2287
2288 type = this._.normalizeType( type );
2289
2290 var data = this._.data[ type ],
2291 result;
2292
2293 if ( isEmpty( data ) ) {
2294 try {
2295 data = this.$.getData( type );
2296 } catch ( e ) {}
2297 }
2298
2299 if ( isEmpty( data ) ) {
2300 data = '';
2301 }
2302
2303 // Some browsers add <meta http-equiv="content-type" content="text/html; charset=utf-8"> at the begging of the HTML data
2304 // or surround it with <html><head>...</head><body>(some content)<!--StartFragment--> and <!--EndFragment-->(some content)</body></html>
2305 // This code removes meta tags and returns only the contents of the <body> element if found. Note that
2306 // some significant content may be placed outside Start/EndFragment comments so it's kept.
2307 //
2308 // See http://dev.ckeditor.com/ticket/13583 for more details.
2309 // Additionally http://dev.ckeditor.com/ticket/16847 adds a flag allowing to get the whole, original content.
2310 if ( type == 'text/html' && !getNative ) {
2311 data = data.replace( this._.metaRegExp, '' );
2312
2313 // Keep only contents of the <body> element
2314 result = this._.bodyRegExp.exec( data );
2315 if ( result && result.length ) {
2316 data = result[ 1 ];
2317
2318 // Remove also comments.
2319 data = data.replace( this._.fragmentRegExp, '' );
2320 }
2321 }
2322 // Firefox on Linux put files paths as a text/plain data if there are files
2323 // in the dataTransfer object. We need to hide it, because files should be
2324 // handled on paste only if dataValue is empty.
2325 else if ( type == 'Text' && CKEDITOR.env.gecko && this.getFilesCount() &&
2326 data.substring( 0, 7 ) == 'file://' ) {
2327 data = '';
2328 }
2329
2330 return filterUnwantedCharacters( data );
2331 },
2332
2333 /**
2334 * Facade for the native `setData` method.
2335 *
2336 * @param {String} type The type of data to retrieve.
2337 * @param {String} value The data to add.
2338 */
2339 setData: function( type, value ) {
2340 type = this._.normalizeType( type );
2341
2342 this._.data[ type ] = value;
2343
2344 // There is "Unexpected call to method or property access." error if you try
2345 // to set data of unsupported type on IE.
2346 if ( !CKEDITOR.plugins.clipboard.isCustomDataTypesSupported && type != 'URL' && type != 'Text' ) {
2347 return;
2348 }
2349
2350 // If we use the text type to bind the ID, then if someone tries to set the text, we must also
2351 // update ID accordingly. http://dev.ckeditor.com/ticket/13468.
2352 if ( clipboardIdDataType == 'Text' && type == 'Text' ) {
2353 this.id = value;
2354 }
2355
2356 try {
2357 this.$.setData( type, value );
2358 } catch ( e ) {}
2359 },
2360
2361 /**
2362 * Gets the data transfer type.
2363 *
2364 * @param {CKEDITOR.editor} targetEditor The drop/paste target editor instance.
2365 * @returns {Number} Possible values: {@link CKEDITOR#DATA_TRANSFER_INTERNAL},
2366 * {@link CKEDITOR#DATA_TRANSFER_CROSS_EDITORS}, {@link CKEDITOR#DATA_TRANSFER_EXTERNAL}.
2367 */
2368 getTransferType: function( targetEditor ) {
2369 if ( !this.sourceEditor ) {
2370 return CKEDITOR.DATA_TRANSFER_EXTERNAL;
2371 } else if ( this.sourceEditor == targetEditor ) {
2372 return CKEDITOR.DATA_TRANSFER_INTERNAL;
2373 } else {
2374 return CKEDITOR.DATA_TRANSFER_CROSS_EDITORS;
2375 }
2376 },
2377
2378 /**
2379 * Copies the data from the native data transfer to a private cache.
2380 * This function is needed because the data from the native data transfer
2381 * is available only synchronously to the event listener. It is not possible
2382 * to get the data asynchronously, after a timeout, and the {@link CKEDITOR.editor#paste}
2383 * event is fired asynchronously &mdash; hence the need for caching the data.
2384 */
2385 cacheData: function() {
2386 if ( !this.$ ) {
2387 return;
2388 }
2389
2390 var that = this,
2391 i, file;
2392
2393 function getAndSetData( type ) {
2394 type = that._.normalizeType( type );
2395
2396 var data = that.getData( type, true );
2397 if ( data ) {
2398 that._.data[ type ] = data;
2399 }
2400 }
2401
2402 // Copy data.
2403 if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ) {
2404 if ( this.$.types ) {
2405 for ( i = 0; i < this.$.types.length; i++ ) {
2406 getAndSetData( this.$.types[ i ] );
2407 }
2408 }
2409 } else {
2410 getAndSetData( 'Text' );
2411 getAndSetData( 'URL' );
2412 }
2413
2414 // Copy files references.
2415 file = this._getImageFromClipboard();
2416 if ( ( this.$ && this.$.files ) || file ) {
2417 this._.files = [];
2418
2419 // Edge have empty files property with no length (http://dev.ckeditor.com/ticket/13755).
2420 if ( this.$.files && this.$.files.length ) {
2421 for ( i = 0; i < this.$.files.length; i++ ) {
2422 this._.files.push( this.$.files[ i ] );
2423 }
2424 }
2425
2426 // Don't include $.items if both $.files and $.items contains files, because,
2427 // according to spec and browsers behavior, they contain the same files.
2428 if ( this._.files.length === 0 && file ) {
2429 this._.files.push( file );
2430 }
2431 }
2432 },
2433
2434 /**
2435 * Gets the number of files in the dataTransfer object.
2436 *
2437 * @returns {Number} The number of files.
2438 */
2439 getFilesCount: function() {
2440 if ( this._.files.length ) {
2441 return this._.files.length;
2442 }
2443
2444 if ( this.$ && this.$.files && this.$.files.length ) {
2445 return this.$.files.length;
2446 }
2447
2448 return this._getImageFromClipboard() ? 1 : 0;
2449 },
2450
2451 /**
2452 * Gets the file at the index given.
2453 *
2454 * @param {Number} i Index.
2455 * @returns {File} File instance.
2456 */
2457 getFile: function( i ) {
2458 if ( this._.files.length ) {
2459 return this._.files[ i ];
2460 }
2461
2462 if ( this.$ && this.$.files && this.$.files.length ) {
2463 return this.$.files[ i ];
2464 }
2465
2466 // File or null if the file was not found.
2467 return i === 0 ? this._getImageFromClipboard() : undefined;
2468 },
2469
2470 /**
2471 * Checks if the data transfer contains any data.
2472 *
2473 * @returns {Boolean} `true` if the object contains no data.
2474 */
2475 isEmpty: function() {
2476 var typesToCheck = {},
2477 type;
2478
2479 // If dataTransfer contains files it is not empty.
2480 if ( this.getFilesCount() ) {
2481 return false;
2482 }
2483
2484 // Add custom types.
2485 for ( type in this._.data ) {
2486 typesToCheck[ type ] = 1;
2487 }
2488
2489 // Add native types.
2490 if ( this.$ ) {
2491 if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ) {
2492 if ( this.$.types ) {
2493 for ( var i = 0; i < this.$.types.length; i++ ) {
2494 typesToCheck[ this.$.types[ i ] ] = 1;
2495 }
2496 }
2497 } else {
2498 typesToCheck.Text = 1;
2499 typesToCheck.URL = 1;
2500 }
2501 }
2502
2503 // Remove ID.
2504 if ( clipboardIdDataType != 'Text' ) {
2505 typesToCheck[ clipboardIdDataType ] = 0;
2506 }
2507
2508 for ( type in typesToCheck ) {
2509 if ( typesToCheck[ type ] && this.getData( type ) !== '' ) {
2510 return false;
2511 }
2512 }
2513
2514 return true;
2515 },
2516
2517 /**
2518 * When the content of the clipboard is pasted in Chrome, the clipboard data object has an empty `files` property,
2519 * but it is possible to get the file as `items[0].getAsFile();` (http://dev.ckeditor.com/ticket/12961).
2520 *
2521 * @private
2522 * @returns {File} File instance or `null` if not found.
2523 */
2524 _getImageFromClipboard: function() {
2525 var file;
2526
2527 if ( this.$ && this.$.items && this.$.items[ 0 ] ) {
2528 try {
2529 file = this.$.items[ 0 ].getAsFile();
2530 // Duck typing
2531 if ( file && file.type ) {
2532 return file;
2533 }
2534 } catch ( err ) {
2535 // noop
2536 }
2537 }
2538
2539 return undefined;
2540 }
2541 };
2542 } )();
2543
2544 /**
2545 * The default content type that is used when pasted data cannot be clearly recognized as HTML or text.
2546 *
2547 * For example: `'foo'` may come from a plain text editor or a website. It is not possible to recognize the content
2548 * type in this case, so the default type will be used. At the same time it is clear that `'<b>example</b> text'` is
2549 * HTML and its origin is a web page, email or another rich text editor.
2550 *
2551 * **Note:** If content type is text, then styles of the paste context are preserved.
2552 *
2553 * CKEDITOR.config.clipboard_defaultContentType = 'text';
2554 *
2555 * See also the {@link CKEDITOR.editor#paste} event and read more about the integration with clipboard
2556 * in the [Clipboard Deep Dive guide](#!/guide/dev_clipboard).
2557 *
2558 * @since 4.0
2559 * @cfg {'html'/'text'} [clipboard_defaultContentType='html']
2560 * @member CKEDITOR.config
2561 */
2562
2563 /**
2564 * Fired after the user initiated a paste action, but before the data is inserted into the editor.
2565 * The listeners to this event are able to process the content before its insertion into the document.
2566 *
2567 * Read more about the integration with clipboard in the [Clipboard Deep Dive guide](#!/guide/dev_clipboard).
2568 *
2569 * See also:
2570 *
2571 * * the {@link CKEDITOR.config#pasteFilter} option,
2572 * * the {@link CKEDITOR.editor#drop} event,
2573 * * the {@link CKEDITOR.plugins.clipboard.dataTransfer} class.
2574 *
2575 * @since 3.1
2576 * @event paste
2577 * @member CKEDITOR.editor
2578 * @param {CKEDITOR.editor} editor This editor instance.
2579 * @param data
2580 * @param {String} data.type The type of data in `data.dataValue`. Usually `'html'` or `'text'`, but for listeners
2581 * with a priority smaller than `6` it may also be `'auto'` which means that the content type has not been recognised yet
2582 * (this will be done by the content type sniffer that listens with priority `6`).
2583 * @param {String} data.dataValue HTML to be pasted.
2584 * @param {String} data.method Indicates the data transfer method. It could be drag and drop or copy and paste.
2585 * Possible values: `'drop'`, `'paste'`. Introduced in CKEditor 4.5.
2586 * @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer Facade for the native dataTransfer object
2587 * which provides access to various data types and files, and passes some data between linked events
2588 * (like drag and drop). Introduced in CKEditor 4.5.
2589 * @param {Boolean} [data.dontFilter=false] Whether the {@link CKEDITOR.editor#pasteFilter paste filter} should not
2590 * be applied to data. This option has no effect when `data.type` equals `'text'` which means that for instance
2591 * {@link CKEDITOR.config#forcePasteAsPlainText} has a higher priority. Introduced in CKEditor 4.5.
2592 */
2593
2594 /**
2595 * Fired before the {@link #paste} event. Allows to preset data type.
2596 *
2597 * **Note:** This event is deprecated. Add a `0` priority listener for the
2598 * {@link #paste} event instead.
2599 *
2600 * @deprecated
2601 * @event beforePaste
2602 * @member CKEDITOR.editor
2603 */
2604
2605 /**
2606 * Fired after the {@link #paste} event if content was modified. Note that if the paste
2607 * event does not insert any data, the `afterPaste` event will not be fired.
2608 *
2609 * @event afterPaste
2610 * @member CKEDITOR.editor
2611 */
2612
2613 /**
2614 * Facade for the native `drop` event. Fired when the native `drop` event occurs.
2615 *
2616 * **Note:** To manipulate dropped data, use the {@link CKEDITOR.editor#paste} event.
2617 * Use the `drop` event only to control drag and drop operations (e.g. to prevent the ability to drop some content).
2618 *
2619 * Read more about integration with drag and drop in the [Clipboard Deep Dive guide](#!/guide/dev_clipboard).
2620 *
2621 * See also:
2622 *
2623 * * The {@link CKEDITOR.editor#paste} event,
2624 * * The {@link CKEDITOR.editor#dragstart} and {@link CKEDITOR.editor#dragend} events,
2625 * * The {@link CKEDITOR.plugins.clipboard.dataTransfer} class.
2626 *
2627 * @since 4.5
2628 * @event drop
2629 * @member CKEDITOR.editor
2630 * @param {CKEDITOR.editor} editor This editor instance.
2631 * @param data
2632 * @param {Object} data.$ Native drop event.
2633 * @param {CKEDITOR.dom.node} data.target Drop target.
2634 * @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade.
2635 * @param {CKEDITOR.dom.range} data.dragRange Drag range, lets you manipulate the drag range.
2636 * Note that dragged HTML is saved as `text/html` data on `dragstart` so if you change the drag range
2637 * on drop, dropped HTML will not change. You need to change it manually using
2638 * {@link CKEDITOR.plugins.clipboard.dataTransfer#setData dataTransfer.setData}.
2639 * @param {CKEDITOR.dom.range} data.dropRange Drop range, lets you manipulate the drop range.
2640 */
2641
2642 /**
2643 * Facade for the native `dragstart` event. Fired when the native `dragstart` event occurs.
2644 *
2645 * This event can be canceled in order to block the drag start operation. It can also be fired to mimic the start of the drag and drop
2646 * operation. For instance, the `widget` plugin uses this option to integrate its custom block widget drag and drop with
2647 * the entire system.
2648 *
2649 * Read more about integration with drag and drop in the [Clipboard Deep Dive guide](#!/guide/dev_clipboard).
2650 *
2651 * See also:
2652 *
2653 * * The {@link CKEDITOR.editor#paste} event,
2654 * * The {@link CKEDITOR.editor#drop} and {@link CKEDITOR.editor#dragend} events,
2655 * * The {@link CKEDITOR.plugins.clipboard.dataTransfer} class.
2656 *
2657 * @since 4.5
2658 * @event dragstart
2659 * @member CKEDITOR.editor
2660 * @param {CKEDITOR.editor} editor This editor instance.
2661 * @param data
2662 * @param {Object} data.$ Native dragstart event.
2663 * @param {CKEDITOR.dom.node} data.target Drag target.
2664 * @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade.
2665 */
2666
2667 /**
2668 * Facade for the native `dragend` event. Fired when the native `dragend` event occurs.
2669 *
2670 * Read more about integration with drag and drop in the [Clipboard Deep Dive guide](#!/guide/dev_clipboard).
2671 *
2672 * See also:
2673 *
2674 * * The {@link CKEDITOR.editor#paste} event,
2675 * * The {@link CKEDITOR.editor#drop} and {@link CKEDITOR.editor#dragend} events,
2676 * * The {@link CKEDITOR.plugins.clipboard.dataTransfer} class.
2677 *
2678 * @since 4.5
2679 * @event dragend
2680 * @member CKEDITOR.editor
2681 * @param {CKEDITOR.editor} editor This editor instance.
2682 * @param data
2683 * @param {Object} data.$ Native dragend event.
2684 * @param {CKEDITOR.dom.node} data.target Drag target.
2685 * @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade.
2686 */
2687
2688 /**
2689 * Defines a filter which is applied to external data pasted or dropped into the editor. Possible values are:
2690 *
2691 * * `'plain-text'` &ndash; Content will be pasted as a plain text.
2692 * * `'semantic-content'` &ndash; Known tags (except `div`, `span`) with all attributes (except
2693 * `style` and `class`) will be kept.
2694 * * `'h1 h2 p div'` &ndash; Custom rules compatible with {@link CKEDITOR.filter}.
2695 * * `null` &ndash; Content will not be filtered by the paste filter (but it still may be filtered
2696 * by [Advanced Content Filter](#!/guide/dev_advanced_content_filter)). This value can be used to
2697 * disable the paste filter in Chrome and Safari, where this option defaults to `'semantic-content'`.
2698 *
2699 * Example:
2700 *
2701 * config.pasteFilter = 'plain-text';
2702 *
2703 * Custom setting:
2704 *
2705 * config.pasteFilter = 'h1 h2 p ul ol li; img[!src, alt]; a[!href]';
2706 *
2707 * Based on this configuration option, a proper {@link CKEDITOR.filter} instance will be defined and assigned to the editor
2708 * as a {@link CKEDITOR.editor#pasteFilter}. You can tweak the paste filter settings on the fly on this object
2709 * as well as delete or replace it.
2710 *
2711 * var editor = CKEDITOR.replace( 'editor', {
2712 * pasteFilter: 'semantic-content'
2713 * } );
2714 *
2715 * editor.on( 'instanceReady', function() {
2716 * // The result of this will be that all semantic content will be preserved
2717 * // except tables.
2718 * editor.pasteFilter.disallow( 'table' );
2719 * } );
2720 *
2721 * Note that the paste filter is applied only to **external** data. There are three data sources:
2722 *
2723 * * copied and pasted in the same editor (internal),
2724 * * copied from one editor and pasted into another (cross-editor),
2725 * * coming from all other sources like websites, MS Word, etc. (external).
2726 *
2727 * If {@link CKEDITOR.config#allowedContent Advanced Content Filter} is not disabled, then
2728 * it will also be applied to pasted and dropped data. The paste filter job is to "normalize"
2729 * external data which often needs to be handled differently than content produced by the editor.
2730 *
2731 * This setting defaults to `'semantic-content'` in Chrome, Opera and Safari (all Blink and Webkit based browsers)
2732 * due to messy HTML which these browsers keep in the clipboard. In other browsers it defaults to `null`.
2733 *
2734 * @since 4.5
2735 * @cfg {String} [pasteFilter='semantic-content' in Chrome and Safari and `null` in other browsers]
2736 * @member CKEDITOR.config
2737 */
2738
2739 /**
2740 * {@link CKEDITOR.filter Content filter} which is used when external data is pasted or dropped into the editor
2741 * or a forced paste as plain text occurs.
2742 *
2743 * This object might be used on the fly to define rules for pasted external content.
2744 * This object is available and used if the {@link CKEDITOR.plugins.clipboard clipboard} plugin is enabled and
2745 * {@link CKEDITOR.config#pasteFilter} or {@link CKEDITOR.config#forcePasteAsPlainText} was defined.
2746 *
2747 * To enable the filter:
2748 *
2749 * var editor = CKEDITOR.replace( 'editor', {
2750 * pasteFilter: 'plain-text'
2751 * } );
2752 *
2753 * You can also modify the filter on the fly later on:
2754 *
2755 * editor.pasteFilter = new CKEDITOR.filter( 'p h1 h2; a[!href]' );
2756 *
2757 * Note that the paste filter is only applied to **external** data. There are three data sources:
2758 *
2759 * * copied and pasted in the same editor (internal),
2760 * * copied from one editor and pasted into another (cross-editor),
2761 * * coming from all other sources like websites, MS Word, etc. (external).
2762 *
2763 * If {@link CKEDITOR.config#allowedContent Advanced Content Filter} is not disabled, then
2764 * it will also be applied to pasted and dropped data. The paste filter job is to "normalize"
2765 * external data which often needs to be handled differently than content produced by the editor.
2766 *
2767 * @since 4.5
2768 * @readonly
2769 * @property {CKEDITOR.filter} [pasteFilter]
2770 * @member CKEDITOR.editor
2771 */
2772
2773 /**
2774 * Duration of the notification displayed after pasting was blocked by the browser.
2775 *
2776 * @since 4.7.0
2777 * @cfg {Number} [clipboard_notificationDuration=10000]
2778 * @member CKEDITOR.config
2779 */
2780 CKEDITOR.config.clipboard_notificationDuration = 10000;