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