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