/** * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or http://ckeditor.com/license */ /** * @ignore * File overview: Clipboard support. */ // // EXECUTION FLOWS: // -- CTRL+C // * browser's default behaviour // -- CTRL+V // * listen onKey (onkeydown) // * simulate 'beforepaste' for non-IEs on editable // * simulate 'paste' for Fx2/Opera on editable // * listen 'onpaste' on editable ('onbeforepaste' for IE) // * fire 'beforePaste' on editor // * !canceled && getClipboardDataByPastebin // * fire 'paste' on editor // * !canceled && fire 'afterPaste' on editor // -- CTRL+X // * listen onKey (onkeydown) // * fire 'saveSnapshot' on editor // * browser's default behaviour // * deferred second 'saveSnapshot' event // -- Copy command // * tryToCutCopy // * execCommand // * !success && alert // -- Cut command // * fixCut // * tryToCutCopy // * execCommand // * !success && alert // -- Paste command // * fire 'paste' on editable ('beforepaste' for IE) // * !canceled && execCommand 'paste' // * !success && fire 'pasteDialog' on editor // -- Paste from native context menu & menubar // (Fx & Webkits are handled in 'paste' default listner. // Opera cannot be handled at all because it doesn't fire any events // Special treatment is needed for IE, for which is this part of doc) // * listen 'onpaste' // * cancel native event // * fire 'beforePaste' on editor // * !canceled && getClipboardDataByPastebin // * execIECommand( 'paste' ) -> this fires another 'paste' event, so cancel it // * fire 'paste' on editor // * !canceled && fire 'afterPaste' on editor // // // PASTE EVENT - PREPROCESSING: // -- Possible dataValue types: auto, text, html. // -- Possible dataValue contents: // * text (possible \n\r) // * htmlified text (text + br,div,p - no presentional markup & attrs - depends on browser) // * html // -- Possible flags: // * htmlified - if true then content is a HTML even if no markup inside. This flag is set // for content from editable pastebins, because they 'htmlify' pasted content. // // -- Type: auto: // * content: htmlified text -> filter, unify text markup (brs, ps, divs), set type: text // * content: html -> filter, set type: html // -- Type: text: // * content: htmlified text -> filter, unify text markup // * content: html -> filter, strip presentional markup, unify text markup // -- Type: html: // * content: htmlified text -> filter, unify text markup // * content: html -> filter // // -- Phases: // * filtering (priorities 3-5) - e.g. pastefromword filters // * content type sniffing (priority 6) // * markup transformations for text (priority 6) // 'use strict'; ( function() { // Register the plugin. CKEDITOR.plugins.add( 'clipboard', { requires: 'dialog', lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE% icons: 'copy,copy-rtl,cut,cut-rtl,paste,paste-rtl', // %REMOVE_LINE_CORE% hidpi: true, // %REMOVE_LINE_CORE% init: function( editor ) { var textificationFilter; initClipboard( editor ); CKEDITOR.dialog.add( 'paste', CKEDITOR.getUrl( this.path + 'dialogs/paste.js' ) ); editor.on( 'paste', function( evt ) { var data = evt.data.dataValue, blockElements = CKEDITOR.dtd.$block; // Filter webkit garbage. if ( data.indexOf( 'Apple-' ) > -1 ) { // Replace special webkit's   with simple space, because webkit // produces them even for normal spaces. data = data.replace( / <\/span>/gi, ' ' ); // Strip around white-spaces when not in forced 'html' content type. // This spans are created only when pasting plain text into Webkit, // but for safety reasons remove them always. if ( evt.data.type != 'html' ) data = data.replace( /]*>([^<]*)<\/span>/gi, function( all, spaces ) { // Replace tabs with 4 spaces like Fx does. return spaces.replace( /\t/g, '    ' ); } ); // This br is produced only when copying & pasting HTML content. if ( data.indexOf( '
' ) > -1 ) { evt.data.startsWithEOL = 1; evt.data.preSniffing = 'html'; // Mark as not text. data = data.replace( /
/, '' ); } // Remove all other classes. data = data.replace( /(<[^>]+) class="Apple-[^"]*"/gi, '$1' ); } // Strip editable that was copied from inside. (#9534) if ( data.match( /^<[^<]+cke_(editable|contents)/i ) ) { var tmp, editable_wrapper, wrapper = new CKEDITOR.dom.element( 'div' ); wrapper.setHtml( data ); // Verify for sure and check for nested editor UI parts. (#9675) while ( wrapper.getChildCount() == 1 && ( tmp = wrapper.getFirst() ) && tmp.type == CKEDITOR.NODE_ELEMENT && // Make sure first-child is element. ( tmp.hasClass( 'cke_editable' ) || tmp.hasClass( 'cke_contents' ) ) ) { wrapper = editable_wrapper = tmp; } // If editable wrapper was found strip it and bogus
(added on FF). if ( editable_wrapper ) data = editable_wrapper.getHtml().replace( /
$/i, '' ); } if ( CKEDITOR.env.ie ) { //  

->

(br.cke-pasted-remove will be removed later) data = data.replace( /^ (?: |\r\n)?<(\w+)/g, function( match, elementName ) { if ( elementName.toLowerCase() in blockElements ) { evt.data.preSniffing = 'html'; // Mark as not a text. return '<' + elementName; } return match; } ); } else if ( CKEDITOR.env.webkit ) { //


->


// We don't mark br, because this situation can happen for htmlified text too. data = data.replace( /<\/(\w+)>

<\/div>$/, function( match, elementName ) { if ( elementName in blockElements ) { evt.data.endsWithEOL = 1; return ''; } return match; } ); } else if ( CKEDITOR.env.gecko ) { // Firefox adds bogus
when user pasted text followed by space(s). data = data.replace( /(\s)
$/, '$1' ); } evt.data.dataValue = data; }, null, null, 3 ); editor.on( 'paste', function( evt ) { var dataObj = evt.data, type = dataObj.type, data = dataObj.dataValue, trueType, // Default is 'html'. defaultType = editor.config.clipboard_defaultContentType || 'html'; // If forced type is 'html' we don't need to know true data type. if ( type == 'html' || dataObj.preSniffing == 'html' ) trueType = 'html'; else trueType = recogniseContentType( data ); // Unify text markup. if ( trueType == 'htmlifiedtext' ) data = htmlifiedTextHtmlification( editor.config, data ); // Strip presentional markup & unify text markup. else if ( type == 'text' && trueType == 'html' ) { // Init filter only if needed and cache it. data = htmlTextification( editor.config, data, textificationFilter || ( textificationFilter = getTextificationFilter( editor ) ) ); } if ( dataObj.startsWithEOL ) data = '
' + data; if ( dataObj.endsWithEOL ) data += '
'; if ( type == 'auto' ) type = ( trueType == 'html' || defaultType == 'html' ) ? 'html' : 'text'; dataObj.type = type; dataObj.dataValue = data; delete dataObj.preSniffing; delete dataObj.startsWithEOL; delete dataObj.endsWithEOL; }, null, null, 6 ); // Inserts processed data into the editor at the end of the // events chain. editor.on( 'paste', function( evt ) { var data = evt.data; editor.insertHtml( data.dataValue, data.type ); // Deferr 'afterPaste' so all other listeners for 'paste' will be fired first. setTimeout( function() { editor.fire( 'afterPaste' ); }, 0 ); }, null, null, 1000 ); editor.on( 'pasteDialog', function( evt ) { // TODO it's possible that this setTimeout is not needed any more, // because of changes introduced in the same commit as this comment. // Editor.getClipboardData adds listner to the dialog's events which are // fired after a while (not like 'showDialog'). setTimeout( function() { // Open default paste dialog. editor.openDialog( 'paste', evt.data ); }, 0 ); } ); } } ); function initClipboard( editor ) { var preventBeforePasteEvent = 0, preventPasteEvent = 0, inReadOnly = 0, // Safari doesn't like 'beforepaste' event - it sometimes doesn't // properly handles ctrl+c. Probably some race-condition between events. // Chrome and Firefox works well with both events, so better to use 'paste' // which will handle pasting from e.g. browsers' menu bars. // IE7/8 doesn't like 'paste' event for which it's throwing random errors. mainPasteEvent = CKEDITOR.env.ie ? 'beforepaste' : 'paste'; addListeners(); addButtonsCommands(); /** * Gets clipboard data by directly accessing the clipboard (IE only) or opening paste dialog. * * editor.getClipboardData( { title: 'Get my data' }, function( data ) { * if ( data ) * alert( data.type + ' ' + data.dataValue ); * } ); * * @member CKEDITOR.editor * @param {Object} options * @param {String} [options.title] Title of paste dialog. * @param {Function} callback Function that will be executed with `data.type` and `data.dataValue` * or `null` if none of the capturing method succeeded. */ editor.getClipboardData = function( options, callback ) { var beforePasteNotCanceled = false, dataType = 'auto', dialogCommited = false; // Options are optional - args shift. if ( !callback ) { callback = options; options = null; } // Listen with maximum priority to handle content before everyone else. // This callback will handle paste event that will be fired if direct // access to the clipboard succeed in IE. editor.on( 'paste', onPaste, null, null, 0 ); // Listen at the end of listeners chain to see if event wasn't canceled // and to retrieve modified data.type. editor.on( 'beforePaste', onBeforePaste, null, null, 1000 ); // getClipboardDataDirectly() will fire 'beforePaste' synchronously, so we can // check if it was canceled and if any listener modified data.type. // If command didn't succeed (only IE allows to access clipboard and only if // user agrees) open and handle paste dialog. if ( getClipboardDataDirectly() === false ) { // Direct access to the clipboard wasn't successful so remove listener. editor.removeListener( 'paste', onPaste ); // If beforePaste was canceled do not open dialog. // Add listeners only if dialog really opened. 'pasteDialog' can be canceled. if ( beforePasteNotCanceled && editor.fire( 'pasteDialog', onDialogOpen ) ) { editor.on( 'pasteDialogCommit', onDialogCommit ); // 'dialogHide' will be fired after 'pasteDialogCommit'. editor.on( 'dialogHide', function( evt ) { evt.removeListener(); evt.data.removeListener( 'pasteDialogCommit', onDialogCommit ); // Because Opera has to wait a while in pasteDialog we have to wait here. setTimeout( function() { // Notify even if user canceled dialog (clicked 'cancel', ESC, etc). if ( !dialogCommited ) callback( null ); }, 10 ); } ); } else callback( null ); } function onPaste( evt ) { evt.removeListener(); evt.cancel(); callback( evt.data ); } function onBeforePaste( evt ) { evt.removeListener(); beforePasteNotCanceled = true; dataType = evt.data.type; } function onDialogCommit( evt ) { evt.removeListener(); // Cancel pasteDialogCommit so paste dialog won't automatically fire // 'paste' evt by itself. evt.cancel(); dialogCommited = true; callback( { type: dataType, dataValue: evt.data } ); } function onDialogOpen() { this.customTitle = ( options && options.title ); } }; function addButtonsCommands() { addButtonCommand( 'Cut', 'cut', createCutCopyCmd( 'cut' ), 10, 1 ); addButtonCommand( 'Copy', 'copy', createCutCopyCmd( 'copy' ), 20, 4 ); addButtonCommand( 'Paste', 'paste', createPasteCmd(), 30, 8 ); function addButtonCommand( buttonName, commandName, command, toolbarOrder, ctxMenuOrder ) { var lang = editor.lang.clipboard[ commandName ]; editor.addCommand( commandName, command ); editor.ui.addButton && editor.ui.addButton( buttonName, { label: lang, command: commandName, toolbar: 'clipboard,' + toolbarOrder } ); // If the "menu" plugin is loaded, register the menu item. if ( editor.addMenuItems ) { editor.addMenuItem( commandName, { label: lang, command: commandName, group: 'clipboard', order: ctxMenuOrder } ); } } } function addListeners() { editor.on( 'key', onKey ); editor.on( 'contentDom', addListenersToEditable ); // For improved performance, we're checking the readOnly state on selectionChange instead of hooking a key event for that. editor.on( 'selectionChange', function( evt ) { inReadOnly = evt.data.selection.getRanges()[ 0 ].checkReadOnly(); setToolbarStates(); } ); // If the "contextmenu" plugin is loaded, register the listeners. if ( editor.contextMenu ) { editor.contextMenu.addListener( function( element, selection ) { inReadOnly = selection.getRanges()[ 0 ].checkReadOnly(); return { cut: stateFromNamedCommand( 'cut' ), copy: stateFromNamedCommand( 'copy' ), paste: stateFromNamedCommand( 'paste' ) }; } ); } } // Add events listeners to editable. function addListenersToEditable() { var editable = editor.editable(); // We'll be catching all pasted content in one line, regardless of whether // it's introduced by a document command execution (e.g. toolbar buttons) or // user paste behaviors (e.g. CTRL+V). editable.on( mainPasteEvent, function( evt ) { if ( CKEDITOR.env.ie && preventBeforePasteEvent ) return; // If you've just asked yourself why preventPasteEventNow() is not here, but // in listener for CTRL+V and exec method of 'paste' command // you've asked the same question we did. // // THE ANSWER: // // First thing to notice - this answer makes sense only for IE, // because other browsers don't listen for 'paste' event. // // What would happen if we move preventPasteEventNow() here? // For: // * CTRL+V - IE fires 'beforepaste', so we prevent 'paste' and pasteDataFromClipboard(). OK. // * editor.execCommand( 'paste' ) - we fire 'beforepaste', so we prevent // 'paste' and pasteDataFromClipboard() and doc.execCommand( 'Paste' ). OK. // * native context menu - IE fires 'beforepaste', so we prevent 'paste', but unfortunately // on IE we fail with pasteDataFromClipboard() here, because of... we don't know why, but // we just fail, so... we paste nothing. FAIL. // * native menu bar - the same as for native context menu. // // But don't you know any way to distinguish first two cases from last two? // Only one - special flag set in CTRL+V handler and exec method of 'paste' // command. And that's what we did using preventPasteEventNow(). pasteDataFromClipboard( evt ); } ); // It's not possible to clearly handle all four paste methods (ctrl+v, native menu bar // native context menu, editor's command) in one 'paste/beforepaste' event in IE. // // For ctrl+v & editor's command it's easy to handle pasting in 'beforepaste' listener, // so we do this. For another two methods it's better to use 'paste' event. // // 'paste' is always being fired after 'beforepaste' (except of weird one on opening native // context menu), so for two methods handled in 'beforepaste' we're canceling 'paste' // using preventPasteEvent state. // // 'paste' event in IE is being fired before getClipboardDataByPastebin executes its callback. // // QUESTION: Why didn't you handle all 4 paste methods in handler for 'paste'? // Wouldn't this just be simpler? // ANSWER: Then we would have to evt.data.preventDefault() only for native // context menu and menu bar pastes. The same with execIECommand(). // That would force us to mark CTRL+V and editor's paste command with // special flag, other than preventPasteEvent. But we still would have to // have preventPasteEvent for the second event fired by execIECommand. // Code would be longer and not cleaner. CKEDITOR.env.ie && editable.on( 'paste', function( evt ) { if ( preventPasteEvent ) return; // Cancel next 'paste' event fired by execIECommand( 'paste' ) // at the end of this callback. preventPasteEventNow(); // Prevent native paste. evt.data.preventDefault(); pasteDataFromClipboard( evt ); // Force IE to paste content into pastebin so pasteDataFromClipboard will work. if ( !execIECommand( 'paste' ) ) editor.openDialog( 'paste' ); } ); // [IE] Dismiss the (wrong) 'beforepaste' event fired on context/toolbar menu open. (#7953) if ( CKEDITOR.env.ie ) { editable.on( 'contextmenu', preventBeforePasteEventNow, null, null, 0 ); editable.on( 'beforepaste', function( evt ) { if ( evt.data && !evt.data.$.ctrlKey ) preventBeforePasteEventNow(); }, null, null, 0 ); } editable.on( 'beforecut', function() { !preventBeforePasteEvent && fixCut( editor ); } ); var mouseupTimeout; // Use editor.document instead of editable in non-IEs for observing mouseup // since editable won't fire the event if selection process started within // iframe and ended out of the editor (#9851). editable.attachListener( CKEDITOR.env.ie ? editable : editor.document.getDocumentElement(), 'mouseup', function() { mouseupTimeout = setTimeout( function() { setToolbarStates(); }, 0 ); } ); // Make sure that deferred mouseup callback isn't executed after editor instance // had been destroyed. This may happen when editor.destroy() is called in parallel // with mouseup event (i.e. a button with onclick callback) (#10219). editor.on( 'destroy', function() { clearTimeout( mouseupTimeout ); } ); editable.on( 'keyup', setToolbarStates ); } // Create object representing Cut or Copy commands. function createCutCopyCmd( type ) { return { type: type, canUndo: type == 'cut', // We can't undo copy to clipboard. startDisabled: true, exec: function( data ) { // Attempts to execute the Cut and Copy operations. function tryToCutCopy( type ) { if ( CKEDITOR.env.ie ) return execIECommand( type ); // non-IEs part try { // Other browsers throw an error if the command is disabled. return editor.document.$.execCommand( type, false, null ); } catch ( e ) { return false; } } this.type == 'cut' && fixCut(); var success = tryToCutCopy( this.type ); if ( !success ) alert( editor.lang.clipboard[ this.type + 'Error' ] ); // Show cutError or copyError. return success; } }; } function createPasteCmd() { return { // Snapshots are done manually by editable.insertXXX methods. canUndo: false, async: true, exec: function( editor, data ) { var fire = function( data, withBeforePaste ) { data && firePasteEvents( data.type, data.dataValue, !!withBeforePaste ); editor.fire( 'afterCommandExec', { name: 'paste', command: cmd, returnValue: !!data } ); }, cmd = this; // Check data precisely - don't open dialog on empty string. if ( typeof data == 'string' ) fire( { type: 'auto', dataValue: data }, 1 ); else editor.getClipboardData( fire ); } }; } function preventPasteEventNow() { preventPasteEvent = 1; // For safety reason we should wait longer than 0/1ms. // We don't know how long execution of quite complex getClipboardData will take // and in for example 'paste' listner execCommand() (which fires 'paste') is called // after getClipboardData finishes. // Luckily, it's impossible to immediately fire another 'paste' event we want to handle, // because we only handle there native context menu and menu bar. setTimeout( function() { preventPasteEvent = 0; }, 100 ); } function preventBeforePasteEventNow() { preventBeforePasteEvent = 1; setTimeout( function() { preventBeforePasteEvent = 0; }, 10 ); } // Tries to execute any of the paste, cut or copy commands in IE. Returns a // boolean indicating that the operation succeeded. // @param {String} command *LOWER CASED* name of command ('paste', 'cut', 'copy'). function execIECommand( command ) { var doc = editor.document, body = doc.getBody(), enabled = false, onExec = function() { enabled = true; }; // The following seems to be the only reliable way to detect that // clipboard commands are enabled in IE. It will fire the // onpaste/oncut/oncopy events only if the security settings allowed // the command to execute. body.on( command, onExec ); // IE6/7: document.execCommand has problem to paste into positioned element. ( CKEDITOR.env.version > 7 ? doc.$ : doc.$.selection.createRange() )[ 'execCommand' ]( command ); body.removeListener( command, onExec ); return enabled; } function firePasteEvents( type, data, withBeforePaste ) { var eventData = { type: type }; if ( withBeforePaste ) { // Fire 'beforePaste' event so clipboard flavor get customized // by other plugins. if ( editor.fire( 'beforePaste', eventData ) === false ) return false; // Event canceled } // The very last guard to make sure the paste has successfully happened. // This check should be done after firing 'beforePaste' because for native paste // 'beforePaste' is by default fired even for empty clipboard. if ( !data ) return false; // Reuse eventData.type because the default one could be changed by beforePaste listeners. eventData.dataValue = data; return editor.fire( 'paste', eventData ); } // Cutting off control type element in IE standards breaks the selection entirely. (#4881) function fixCut() { if ( !CKEDITOR.env.ie || CKEDITOR.env.quirks ) return; var sel = editor.getSelection(), control, range, dummy; if ( ( sel.getType() == CKEDITOR.SELECTION_ELEMENT ) && ( control = sel.getSelectedElement() ) ) { range = sel.getRanges()[ 0 ]; dummy = editor.document.createText( '' ); dummy.insertBefore( control ); range.setStartBefore( dummy ); range.setEndAfter( control ); sel.selectRanges( [ range ] ); // Clear up the fix if the paste wasn't succeeded. setTimeout( function() { // Element still online? if ( control.getParent() ) { dummy.remove(); sel.selectElement( control ); } }, 0 ); } } // Allow to peek clipboard content by redirecting the // pasting content into a temporary bin and grab the content of it. function getClipboardDataByPastebin( evt, callback ) { var doc = editor.document, editable = editor.editable(), cancel = function( evt ) { evt.cancel(); }, ff3x = CKEDITOR.env.gecko && CKEDITOR.env.version <= 10902, blurListener; // Avoid recursions on 'paste' event or consequent paste too fast. (#5730) if ( doc.getById( 'cke_pastebin' ) ) return; var sel = editor.getSelection(); var bms = sel.createBookmarks(); // Create container to paste into. // For rich content we prefer to use "body" since it holds // the least possibility to be splitted by pasted content, while this may // breaks the text selection on a frame-less editable, "div" would be // the best one in that case. // In another case on old IEs moving the selection into a "body" paste bin causes error panic. // Body can't be also used for Opera which fills it with
// what is indistinguishable from pasted
(copying
in Opera isn't possible, // but it can be copied from other browser). var pastebin = new CKEDITOR.dom.element( ( CKEDITOR.env.webkit || editable.is( 'body' ) ) && !( CKEDITOR.env.ie || CKEDITOR.env.opera ) ? 'body' : 'div', doc ); pastebin.setAttributes( { id: 'cke_pastebin', 'data-cke-temp': '1' } ); // Append bogus to prevent Opera from doing this. (#9522) if ( CKEDITOR.env.opera ) pastebin.appendBogus(); var containerOffset = 0, offsetParent, win = doc.getWindow(); // Seems to be the only way to avoid page scroll in Fx 3.x. if ( ff3x ) { pastebin.insertAfter( bms[ 0 ].startNode ); pastebin.setStyle( 'display', 'inline' ); } else { if ( CKEDITOR.env.webkit ) { // It's better to paste close to the real paste destination, so inherited styles // (which Webkits will try to compensate by styling span) differs less from the destination's one. editable.append( pastebin ); // Style pastebin like .cke_editable, to minimize differences between origin and destination. (#9754) pastebin.addClass( 'cke_editable' ); // Compensate position of offsetParent. if ( !editable.is( 'body' ) ) { // We're not able to get offsetParent from pastebin (body element), so check whether // its parent (editable) is positioned. if ( editable.getComputedStyle( 'position' ) != 'static' ) offsetParent = editable; // And if not - safely get offsetParent from editable. else offsetParent = CKEDITOR.dom.element.get( editable.$.offsetParent ); containerOffset = offsetParent.getDocumentPosition().y; } } else { // Opera and IE doesn't allow to append to html element. editable.getAscendant( CKEDITOR.env.ie || CKEDITOR.env.opera ? 'body' : 'html', 1 ).append( pastebin ); } pastebin.setStyles( { position: 'absolute', // Position the bin at the top (+10 for safety) of viewport to avoid any subsequent document scroll. top: ( win.getScrollPosition().y - containerOffset + 10 ) + 'px', width: '1px', // Caret has to fit in that height, otherwise browsers like Chrome & Opera will scroll window to show it. // Set height equal to viewport's height - 20px (safety gaps), minimum 1px. height: Math.max( 1, win.getViewPaneSize().height - 20 ) + 'px', overflow: 'hidden', // Reset styles that can mess up pastebin position. margin: 0, padding: 0 } ); } // Check if the paste bin now establishes new editing host. var isEditingHost = pastebin.getParent().isReadOnly(); if ( isEditingHost ) { // Hide the paste bin. pastebin.setOpacity( 0 ); // And make it editable. pastebin.setAttribute( 'contenteditable', true ); } // Transparency is not enough since positioned non-editing host always shows // resize handler, pull it off the screen instead. else pastebin.setStyle( editor.config.contentsLangDirection == 'ltr' ? 'left' : 'right', '-1000px' ); editor.on( 'selectionChange', cancel, null, null, 0 ); // Webkit fill fire blur on editable when moving selection to // pastebin (if body is used). Cancel it because it causes incorrect // selection lock in case of inline editor (#10644). // The same seems to apply to Firefox (#10787). if ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) blurListener = editable.once( 'blur', cancel, null, null, -100 ); // Temporarily move selection to the pastebin. isEditingHost && pastebin.focus(); var range = new CKEDITOR.dom.range( pastebin ); range.selectNodeContents( pastebin ); var selPastebin = range.select(); // If non-native paste is executed, IE will open security alert and blur editable. // Editable will then lock selection inside itself and after accepting security alert // this selection will be restored. We overwrite stored selection, so it's restored // in pastebin. (#9552) if ( CKEDITOR.env.ie ) { blurListener = editable.once( 'blur', function( evt ) { editor.lockSelection( selPastebin ); } ); } var scrollTop = CKEDITOR.document.getWindow().getScrollPosition().y; // Wait a while and grab the pasted contents. setTimeout( function() { // Restore main window's scroll position which could have been changed // by browser in cases described in #9771. if ( CKEDITOR.env.webkit || CKEDITOR.env.opera ) CKEDITOR.document[ CKEDITOR.env.webkit ? 'getBody' : 'getDocumentElement' ]().$.scrollTop = scrollTop; // Blur will be fired only on non-native paste. In other case manually remove listener. blurListener && blurListener.removeListener(); // Restore properly the document focus. (#8849) if ( CKEDITOR.env.ie ) editable.focus(); // IE7: selection must go before removing pastebin. (#8691) sel.selectBookmarks( bms ); pastebin.remove(); // Grab the HTML contents. // We need to look for a apple style wrapper on webkit it also adds // a div wrapper if you copy/paste the body of the editor. // Remove hidden div and restore selection. var bogusSpan; if ( CKEDITOR.env.webkit && ( bogusSpan = pastebin.getFirst() ) && ( bogusSpan.is && bogusSpan.hasClass( 'Apple-style-span' ) ) ) pastebin = bogusSpan; editor.removeListener( 'selectionChange', cancel ); callback( pastebin.getHtml() ); }, 0 ); } // Try to get content directly from clipboard, without native event // being fired before. In other words - synthetically get clipboard data // if it's possible. // mainPasteEvent will be fired, so if forced native paste: // * worked, getClipboardDataByPastebin will grab it, // * didn't work, pastebin will be empty and editor#paste won't be fired. function getClipboardDataDirectly() { if ( CKEDITOR.env.ie ) { // Prevent IE from pasting at the begining of the document. editor.focus(); // Command will be handled by 'beforepaste', but as // execIECommand( 'paste' ) will fire also 'paste' event // we're canceling it. preventPasteEventNow(); // #9247: Lock focus to prevent IE from hiding toolbar for inline editor. var focusManager = editor.focusManager; focusManager.lock(); if ( editor.editable().fire( mainPasteEvent ) && !execIECommand( 'paste' ) ) { focusManager.unlock(); return false; } focusManager.unlock(); } else { try { if ( editor.editable().fire( mainPasteEvent ) && !editor.document.$.execCommand( 'Paste', false, null ) ) throw 0; } catch ( e ) { return false; } } return true; } // Listens for some clipboard related keystrokes, so they get customized. // Needs to be bind to keydown event. function onKey( event ) { if ( editor.mode != 'wysiwyg' ) return; switch ( event.data.keyCode ) { // Paste case CKEDITOR.CTRL + 86: // CTRL+V case CKEDITOR.SHIFT + 45: // SHIFT+INS var editable = editor.editable(); // Cancel 'paste' event because ctrl+v is for IE handled // by 'beforepaste'. preventPasteEventNow(); // Simulate 'beforepaste' event for all none-IEs. !CKEDITOR.env.ie && editable.fire( 'beforepaste' ); // Simulate 'paste' event for Opera/Firefox2. if ( CKEDITOR.env.opera || CKEDITOR.env.gecko && CKEDITOR.env.version < 10900 ) editable.fire( 'paste' ); return; // Cut case CKEDITOR.CTRL + 88: // CTRL+X case CKEDITOR.SHIFT + 46: // SHIFT+DEL // Save Undo snapshot. editor.fire( 'saveSnapshot' ); // Save before cut setTimeout( function() { editor.fire( 'saveSnapshot' ); // Save after cut }, 50 ); // OSX is slow (#11416). } } function pasteDataFromClipboard( evt ) { // Default type is 'auto', but can be changed by beforePaste listeners. var eventData = { type: 'auto' }; // Fire 'beforePaste' event so clipboard flavor get customized by other plugins. // If 'beforePaste' is canceled continue executing getClipboardDataByPastebin and then do nothing // (do not fire 'paste', 'afterPaste' events). This way we can grab all - synthetically // and natively pasted content and prevent its insertion into editor // after canceling 'beforePaste' event. var beforePasteNotCanceled = editor.fire( 'beforePaste', eventData ); getClipboardDataByPastebin( evt, function( data ) { // Clean up. data = data.replace( /]+data-cke-bookmark[^<]*?<\/span>/ig, '' ); // Fire remaining events (without beforePaste) beforePasteNotCanceled && firePasteEvents( eventData.type, data, 0, 1 ); } ); } function setToolbarStates() { if ( editor.mode != 'wysiwyg' ) return; var pasteState = stateFromNamedCommand( 'paste' ); editor.getCommand( 'cut' ).setState( stateFromNamedCommand( 'cut' ) ); editor.getCommand( 'copy' ).setState( stateFromNamedCommand( 'copy' ) ); editor.getCommand( 'paste' ).setState( pasteState ); editor.fire( 'pasteState', pasteState ); } function stateFromNamedCommand( command ) { if ( inReadOnly && command in { paste: 1, cut: 1 } ) return CKEDITOR.TRISTATE_DISABLED; if ( command == 'paste' ) return CKEDITOR.TRISTATE_OFF; // Cut, copy - check if the selection is not empty. var sel = editor.getSelection(), ranges = sel.getRanges(), selectionIsEmpty = sel.getType() == CKEDITOR.SELECTION_NONE || ( ranges.length == 1 && ranges[ 0 ].collapsed ); return selectionIsEmpty ? CKEDITOR.TRISTATE_DISABLED : CKEDITOR.TRISTATE_OFF; } } // Returns: // * 'htmlifiedtext' if content looks like transformed by browser from plain text. // See clipboard/paste.html TCs for more info. // * 'html' if it is not 'htmlifiedtext'. function recogniseContentType( data ) { if ( CKEDITOR.env.webkit ) { // Plain text or (

and text inside
). if ( !data.match( /^[^<]*$/g ) && !data.match( /^(
<\/div>|
[^<]*<\/div>)*$/gi ) ) return 'html'; } else if ( CKEDITOR.env.ie ) { // Text and
or ( text and
in

- paragraphs can be separated by new \r\n ). if ( !data.match( /^([^<]|)*$/gi ) && !data.match( /^(

([^<]|)*<\/p>|(\r\n))*$/gi ) ) return 'html'; } else if ( CKEDITOR.env.gecko || CKEDITOR.env.opera ) { // Text or
. if ( !data.match( /^([^<]|)*$/gi ) ) return 'html'; } else return 'html'; return 'htmlifiedtext'; } // This function transforms what browsers produce when // pasting plain text into editable element (see clipboard/paste.html TCs // for more info) into correct HTML (similar to that produced by text2Html). function htmlifiedTextHtmlification( config, data ) { function repeatParagraphs( repeats ) { // Repeat blocks floor((n+1)/2) times. // Even number of repeats - add
at the beginning of last

. return CKEDITOR.tools.repeat( '

', ~~ ( repeats / 2 ) ) + ( repeats % 2 == 1 ? '
' : '' ); } // Replace adjacent white-spaces (EOLs too - Fx sometimes keeps them) with one space. data = data.replace( /\s+/g, ' ' ) // Remove spaces from between tags. .replace( /> +<' ) // Normalize XHTML syntax and upper cased
tags. .replace( /
/gi, '
' ); // IE - lower cased tags. data = data.replace( /<\/?[A-Z]+>/g, function( match ) { return match.toLowerCase(); } ); // Don't touch single lines (no ) - nothing to do here. if ( data.match( /^[^<]$/ ) ) return data; // Webkit. if ( CKEDITOR.env.webkit && data.indexOf( '

' ) > -1 ) { // One line break at the beginning - insert
data = data.replace( /^(
(
|)<\/div>)(?!$|(
(
|)<\/div>))/g, '
' ) // Two or more - reduce number of new lines by one. .replace( /^(
(
|)<\/div>){2}(?!$)/g, '
' ); // Two line breaks create one paragraph in Webkit. if ( data.match( /
(
|)<\/div>/ ) ) { data = '

' + data.replace( /(

(
|)<\/div>)+/g, function( match ) { return repeatParagraphs( match.split( '
' ).length + 1 ); } ) + '

'; } // One line break create br. data = data.replace( /<\/div>
/g, '
' ); // Remove remaining divs. data = data.replace( /<\/?div>/g, '' ); } // Opera and Firefox and enterMode != BR. if ( ( CKEDITOR.env.gecko || CKEDITOR.env.opera ) && config.enterMode != CKEDITOR.ENTER_BR ) { // Remove bogus
- Fx generates two for one line break. // For two line breaks it still produces two , but it's better to ignore this case than the first one. if ( CKEDITOR.env.gecko ) data = data.replace( /^

$/, '
' ); // This line satisfy edge case when for Opera we have two line breaks //data = data.replace( /) if ( data.indexOf( '

' ) > -1 ) { // Two line breaks create one paragraph, three - 2, four - 3, etc. data = '

' + data.replace( /(
){2,}/g, function( match ) { return repeatParagraphs( match.length / 4 ); } ) + '

'; } } return switchEnterMode( config, data ); } // Filter can be editor dependent. function getTextificationFilter( editor ) { var filter = new CKEDITOR.htmlParser.filter(); // Elements which creates vertical breaks (have vert margins) - took from HTML5 spec. // http://dev.w3.org/html5/markup/Overview.html#toc var replaceWithParaIf = { blockquote: 1, dl: 1, fieldset: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ol: 1, p: 1, table: 1, ul: 1 }, // All names except of
. stripInlineIf = CKEDITOR.tools.extend( { br: 0 }, CKEDITOR.dtd.$inline ), // What's finally allowed (cke:br will be removed later). allowedIf = { p: 1, br: 1, 'cke:br': 1 }, knownIf = CKEDITOR.dtd, // All names that will be removed (with content). removeIf = CKEDITOR.tools.extend( { area: 1, basefont: 1, embed: 1, iframe: 1, map: 1, object: 1, param: 1 }, CKEDITOR.dtd.$nonBodyContent, CKEDITOR.dtd.$cdata ); var flattenTableCell = function( element ) { delete element.name; element.add( new CKEDITOR.htmlParser.text( ' ' ) ); }, // Squash adjacent headers into one.

A

B

->

A
B

// Empty ones will be removed later. squashHeader = function( element ) { var next = element, br, el; while ( ( next = next.next ) && next.name && next.name.match( /^h\d$/ ) ) { // TODO shitty code - waitin' for htmlParse.element fix. br = new CKEDITOR.htmlParser.element( 'cke:br' ); br.isEmpty = true; element.add( br ); while ( ( el = next.children.shift() ) ) element.add( el ); } }; filter.addRules( { elements: { h1: squashHeader, h2: squashHeader, h3: squashHeader, h4: squashHeader, h5: squashHeader, h6: squashHeader, img: function( element ) { var alt = CKEDITOR.tools.trim( element.attributes.alt || '' ), txt = ' '; // Replace image with its alt if it doesn't look like an url or is empty. if ( alt && !alt.match( /(^http|\.(jpe?g|gif|png))/i ) ) txt = ' [' + alt + '] '; return new CKEDITOR.htmlParser.text( txt ); }, td: flattenTableCell, th: flattenTableCell, $: function( element ) { var initialName = element.name, br; // Remove entirely. if ( removeIf[ initialName ] ) return false; // Remove all attributes. element.attributes = {}; // Pass brs. if ( initialName == 'br' ) return element; // Elements that we want to replace with paragraphs. if ( replaceWithParaIf[ initialName ] ) element.name = 'p'; // Elements that we want to strip (tags only, without the content). else if ( stripInlineIf[ initialName ] ) delete element.name; // Surround other known element with and strip tags. else if ( knownIf[ initialName ] ) { // TODO shitty code - waitin' for htmlParse.element fix. br = new CKEDITOR.htmlParser.element( 'cke:br' ); br.isEmpty = true; // Replace hrs (maybe sth else too?) with only one br. if ( CKEDITOR.dtd.$empty[ initialName ] ) return br; element.add( br, 0 ); br = br.clone(); br.isEmpty = true; element.add( br ); delete element.name; } // Final cleanup - if we can still find some not allowed elements then strip their names. if ( !allowedIf[ element.name ] ) delete element.name; return element; } } }, { // Apply this filter to every element. applyToAll: true } ); return filter; } function htmlTextification( config, data, filter ) { var fragment = new CKEDITOR.htmlParser.fragment.fromHtml( data ), writer = new CKEDITOR.htmlParser.basicWriter(); fragment.writeHtml( writer, filter ); data = writer.getHtml(); // Cleanup cke:brs. data = data.replace( /\s*(<\/?[a-z:]+ ?\/?>)\s*/g, '$1' ) // Remove spaces around tags. .replace( /(){2,}/g, '' ) // Join multiple adjacent cke:brs .replace( /()(<\/?p>|
)/g, '$2' ) // Strip cke:brs adjacent to original brs or ps. .replace( /(<\/?p>|
)()/g, '$1' ) .replace( /<(cke:)?br( \/)?>/g, '
' ) // Finally - rename cke:brs to brs and fix
to
. .replace( /

<\/p>/g, '' ); // Remove empty paragraphs. // Fix nested ps. E.g.: //

A

B

C

D

E

F

G //

A

B

C

D

E

F

G var nested = 0; data = data.replace( /<\/?p>/g, function( match ) { if ( match == '

' ) { if ( ++nested > 1 ) return '

'; } else { if ( --nested > 0 ) return '

'; } return match; } ).replace( /

<\/p>/g, '' ); // Step before:

->

. Fix this here. return switchEnterMode( config, data ); } function switchEnterMode( config, data ) { if ( config.enterMode == CKEDITOR.ENTER_BR ) { data = data.replace( /(<\/p>

)+/g, function( match ) { return CKEDITOR.tools.repeat( '
', match.length / 7 * 2 ); } ).replace( /<\/?p>/g, '' ); } else if ( config.enterMode == CKEDITOR.ENTER_DIV ) data = data.replace( /<(\/)?p>/g, '<$1div>' ); return data; } } )(); /** * The default content type is used when pasted data cannot be clearly recognized as HTML or text. * * For example: `'foo'` may come from a plain text editor or a website. It isn't possible to recognize content * type in this case, so default will be used. However, it's clear that `'example text'` is an HTML * and its origin is webpage, email or other rich text editor. * * **Note:** If content type is text, then styles of context of paste are preserved. * * CKEDITOR.config.clipboard_defaultContentType = 'text'; * * @since 4.0 * @cfg {'html'/'text'} [clipboard_defaultContentType='html'] * @member CKEDITOR.config */ /** * Fired when a clipboard operation is about to be taken into the editor. * Listeners can manipulate the data to be pasted before having it effectively * inserted into the document. * * @since 3.1 * @event paste * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. * @param data * @param {String} data.type Type of data in `data.dataValue`. Usually `html` or `text`, but for listeners * with priority less than 6 it may be also `auto`, what means that content type hasn't been recognised yet * (this will be done by content type sniffer that listens with priority 6). * @param {String} data.dataValue HTML to be pasted. */ /** * Internal event to open the Paste dialog. * * @private * @event pasteDialog * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. * @param {Function} [data] Callback that will be passed to {@link CKEDITOR.editor#openDialog}. */