道招

CKEditor系列(三)粘贴操作是怎么完成的

如果您发现本文排版有问题,可以先点击下面的链接切换至老版进行查看!!!

CKEditor系列(三)粘贴操作是怎么完成的

在上一篇文章CKEditor系列(二)事件系统是怎么实现的中,我们了解了CKEditor中事件系统的运行流程,我们先简单回顾下:

  • 用户注册回调函数时可以指定优先级,值越小的优先级越高,默认是10
  • 系统会根据用户的传参组装成系统规范的回调函数,供后续执行
  • 执行回调函数时可以将取消事件和阻止事件,不让其它监听该事件的回调函数执行。

当插件希望对paste事件进行响应,一般有两种方式可供选择。

直接监听'paste'事件

默认情况下,插件clipboard插件是监听paste事件最多的。 我们可以看到里面多次出现类似这样的代码

// plugins/clipboard/plugin.js
editor.on( 'paste', function( evt ) {

})

我们可以看到里面有几个优先级priority 为1回调

处理粘贴图片的场景

将png、jpg、gif图片的内容base64信息赋值给evt.data.dataValue

editor.on( 'paste', function( evt ) {
    var dataObj = evt.data,
        data = dataObj.dataValue,
        dataTransfer = dataObj.dataTransfer;

    // If data empty check for image content inside data transfer. https://dev.ckeditor.com/ticket/16705
    // Allow both dragging and dropping and pasting images as base64 (#4681).
    if ( !data && isFileData( evt, dataTransfer ) ) {
        var file = dataTransfer.getFile( 0 );
        if ( CKEDITOR.tools.indexOf( supportedImageTypes, file.type ) != -1 ) {
            var fileReader = new FileReader();

            // Convert image file to img tag with base64 image.
            fileReader.addEventListener( 'load', function() {
                evt.data.dataValue = '<img src="' + fileReader.result + '" />';
                editor.fire( 'paste', evt.data );
            }, false );

            // Proceed with normal flow if reading file was aborted.
            fileReader.addEventListener( 'abort', function() {
                // (#4681)
                setCustomIEEventAttribute( evt );
                editor.fire( 'paste', evt.data );
            }, false );

            // Proceed with normal flow if reading file failed.
            fileReader.addEventListener( 'error', function() {
                // (#4681)
                setCustomIEEventAttribute( evt );
                editor.fire( 'paste', evt.data );
            }, false );

            fileReader.readAsDataURL( file );

            latestId = dataObj.dataTransfer.id;

            evt.stop();
        }
    }
}, null, null, 1 );

因为base64信息需要通过fileReader来处理:在图片的load回调里面才能拿到,所以我们需要先执行evt.stop(),避免其它回调被执行了,然后在图片load的回调里面重新触发一直paste事件 editor.fire( 'paste', evt.data );,对应的aborterror也要触发,避免因图片失败,导致其它回调都没机会执行了。

该回调会在下一轮paste回调执行中再次执行吗?不会,因为该回调首次执行时evt.data.dataValue为空,下次执行时evt.data.dataValue已经被上次执行给赋值了,不会重复执行fileReader相关处理了。

数据准备
editor.on( 'paste', function( evt ) {
    // Init `dataTransfer` if `paste` event was fired without it, so it will be always available.
    if ( !evt.data.dataTransfer ) {
        evt.data.dataTransfer = new CKEDITOR.plugins.clipboard.dataTransfer();
    }

    // If dataValue is already set (manually or by paste bin), so do not override it.
    if ( evt.data.dataValue ) {
        return;
    }

    var dataTransfer = evt.data.dataTransfer,
        // IE support only text data and throws exception if we try to get html data.
        // This html data object may also be empty if we drag content of the textarea.
        value = dataTransfer.getData( 'text/html' );

    if ( value ) {
        evt.data.dataValue = value;
        evt.data.type = 'html';
    } else {
        // Try to get text data otherwise.
        value = dataTransfer.getData( 'text/plain' );

        if ( value ) {
            evt.data.dataValue = editor.editable().transformPlainTextToHtml( value );
            evt.data.type = 'text';
        }
    }
}, null, null, 1 );

可以看到这个回调函数是主要是给evt.data增加dataTransferdataValue(如果已经被其它插件设置了就直接return出去)和type的,是做准备工作的,所以这个回调函数自然需要最先执行,优先级设置为1。

看看第二个回调函数

解决兼容性
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 class="Apple-converted-space"> <\/span>/gi, ' ' );

        // Strip <span> 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 class="Apple-tab-span"[^>]*>([^<]*)<\/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( '<br class="Apple-interchange-newline">' ) > -1 ) {
            evt.data.startsWithEOL = 1;
            evt.data.preSniffing = 'html'; // Mark as not text.
            data = data.replace( /<br class="Apple-interchange-newline">/, '' );
        }

        // Remove all other classes.
        data = data.replace( /(<[^>]+) class="Apple-[^"]*"/gi, '$1' );
    }

    // Strip editable that was copied from inside. (https://dev.ckeditor.com/ticket/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. (https://dev.ckeditor.com/ticket/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 <br> (added on FF).
        if ( editable_wrapper )
            data = editable_wrapper.getHtml().replace( /<br>$/i, '' );
    }

    if ( CKEDITOR.env.ie ) {
        //   <p> -> <p> (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 ) {
        // </p><div><br></div> -> </p><br>
        // We don't mark br, because this situation can happen for htmlified text too.
        data = data.replace( /<\/(\w+)><div><br><\/div>$/, function( match, elementName ) {
            if ( elementName in blockElements ) {
                evt.data.endsWithEOL = 1;
                return '</' + elementName + '>';
            }
            return match;
        } );
    } else if ( CKEDITOR.env.gecko ) {
        // Firefox adds bogus <br> when user pasted text followed by space(s).
        data = data.replace( /(\s)<br>$/, '$1' );
    }

    evt.data.dataValue = data;
}, null, null, 3 );

从上面的代码很容易看出,主要是针对不同的浏览器做一下兼容性相关的处理,具体细节我们不用太关心

针对不同粘贴源进行数据过滤
editor.on( 'paste', function( evt ) {
    var dataObj = evt.data,
        type = editor._.nextPasteType || dataObj.type,
        data = dataObj.dataValue,
        trueType,
        // Default is 'html'.
        defaultType = editor.config.clipboard_defaultContentType || 'html',
        transferType = dataObj.dataTransfer.getTransferType( editor ),
        isExternalPaste = transferType == CKEDITOR.DATA_TRANSFER_EXTERNAL,
        isActiveForcePAPT = editor.config.forcePasteAsPlainText === true;

    // 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 );
    }

    delete editor._.nextPasteType;

    // Unify text markup.
    if ( trueType == 'htmlifiedtext' ) {
        data = htmlifiedTextHtmlification( editor.config, data );
    }

    // Strip presentational markup & unify text markup.
    // Forced plain text (dialog or forcePAPT).
    // Note: we do not check dontFilter option in this case, because forcePAPT was implemented
    // before pasteFilter and pasteFilter is automatically used on Webkit&Blink since 4.5, so
    // forcePAPT should have priority as it had before 4.5.
    if ( type == 'text' && trueType == 'html' ) {
        data = filterContent( editor, data, filtersFactory.get( 'plain-text' ) );
    }
    // External paste and pasteFilter exists and filtering isn't disabled.
    // Or force filtering even for internal and cross-editor paste, when forcePAPT is active (#620).
    else if ( isExternalPaste && editor.pasteFilter && !dataObj.dontFilter || isActiveForcePAPT ) {
        data = filterContent( editor, data, editor.pasteFilter );
    }

    if ( dataObj.startsWithEOL ) {
        data = '<br data-cke-eol="1">' + data;
    }
    if ( dataObj.endsWithEOL ) {
        data += '<br data-cke-eol="1">';
    }

    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 );

这个主要是根据不同的typetrueType来对数据进行一些过滤操作

插入粘贴数据

粘贴的数据总得进入到编辑器吧,这就靠它了。

editor.on( 'paste', function( evt ) {
    var data = evt.data;
    if ( data.dataValue ) {
        editor.insertHtml( data.dataValue, data.type, data.range );

        // Defer 'afterPaste' so all other listeners for 'paste' will be fired first.
        // Fire afterPaste only if paste inserted some HTML.
        setTimeout( function() {
            editor.fire( 'afterPaste' );
        }, 0 );
    }
}, null, null, 1000 );

这个就比较简单了,但是也很重要,等paste事件系统的回调函数和用户添加的回调函数执行完毕后,这个回调函数作为最后执行的(如果前面的回调函数没有执行evt.stop()或者evt.cancel()),将evt.data.dataValue的值插入到编辑器中。

我们可以再多看一下/plugins/clipboard/plugin.js文件,里面有个对工具栏增加粘贴按钮,加上pasteCommand的操作

{

    exec: function( editor, data ) {
    data = typeof data !== 'undefined' && data !== null ? data : {};

    var cmd = this,
        notification = typeof data.notification !== 'undefined' ? data.notification : true,
        forcedType = data.type,
        keystroke = CKEDITOR.tools.keystrokeToString( editor.lang.common.keyboard,
            editor.getCommandKeystroke( this ) ),
        msg = typeof notification === 'string' ? notification : editor.lang.clipboard.pasteNotification
            .replace( /%1/, '<kbd aria-label="' + keystroke.aria + '">' + keystroke.display + '</kbd>' ),
        pastedContent = typeof data === 'string' ? data : data.dataValue;

    function callback( data, withBeforePaste ) {
        withBeforePaste = typeof withBeforePaste !== 'undefined' ? withBeforePaste : true;

        if ( data ) {
            data.method = 'paste';

            if ( !data.dataTransfer ) {
                data.dataTransfer = clipboard.initPasteDataTransfer();
            }

            firePasteEvents( editor, data, withBeforePaste );
        } else if ( notification && !editor._.forcePasteDialog ) {
            editor.showNotification( msg, 'info', editor.config.clipboard_notificationDuration );
        }

        // Reset dialog mode (#595).
        editor._.forcePasteDialog = false;

        editor.fire( 'afterCommandExec', {
            name: 'paste',
            command: cmd,
            returnValue: !!data
        } );
    }

    // Force type for the next paste. Do not force if `config.forcePasteAsPlainText` set to true or 'allow-word' (#1013).
    if ( forcedType && editor.config.forcePasteAsPlainText !== true && editor.config.forcePasteAsPlainText !== 'allow-word' ) {
        editor._.nextPasteType = forcedType;
    } else {
        delete editor._.nextPasteType;
    }

    if ( typeof pastedContent === 'string' ) {
        callback( {
            dataValue: pastedContent
        } );
    } else {
        editor.getClipboardData( callback );
    }
}

上面的callback会执行firePasteEvents,然后触发paste事件。 如果pastedContent不是字符串的话,会先执行 editor.getClipboardData,该方法中有一个目前看到的优先级最好的paste回调

editor.on( 'paste', onPaste, null, null, 0 );

function onPaste( evt ) {
    evt.removeListener();
    evt.cancel();
    callback( evt.data );
}

onPaste方法里面会移除当前的回调函数,并取消掉后面未执行的paste回调,然后执行callback,也就是说它会触发一轮新的paste回调函数执行。

通过pasteTools插件来注册paste回调

{
    register: function(definition) {
        if (typeof definition.priority !== 'number')
        {
            definition.priority = 10;
        }

        this.handlers.push(definition);
    },
    addPasteListener: function( editor ) {
        editor.on( 'paste', function( evt ) {
            var handlers = getMatchingHandlers( this.handlers, evt ),
                filters,
                isLoaded;

            if ( handlers.length === 0 ) {
                return;
            }

            filters = getFilters( handlers );

            isLoaded = loadFilters( filters, function() {
                return editor.fire( 'paste', evt.data );
            } );

            if ( !isLoaded ) {
                return evt.cancel();
            }

            handlePaste( handlers, evt );
        }, this, null, 3 );
    }
}
...
function getMatchingHandlers( handlers, evt ) {
    return CKEDITOR.tools.array.filter( handlers, function( handler ) {
        return handler.canHandle( evt );
    } ).sort( function( handler1, handler2 ) {
        if ( handler1.priority === handler2.priority ) {
            return 0;
        }

        return handler1.priority - handler2.priority;
    } );
}

function handlePaste( handlers, evt ) {
    var handler = handlers.shift();

    if ( !handler ) {
        return;
    }

    handler.handle( evt, function() {
        handlePaste( handlers, evt );
    } );
}

这个会把通过它注册的回调函数放进自己的handlers里面,而不跟上面那些直接监听paste放在一起,只有该组件自身才监听paste事件,优先级为3。这等于是将通过pasteTools.register注册的这一组回调全部按照了优先级为3的顺序来执行了,当然,这一组的回调直接同样按照优先级高低来执行,并且会根据其canHandle方法返回的值来过滤该回调是否执行,通过其handle来执行回调逻辑。

通过对源码的搜索,发现CKEditor大部分官方提供的对粘贴进行干预的插件都是通过pasteTools.register注册的。

总结

通过对pasteTools插件的学习,我们可以对自己想做系统级事件和用户级事件的分离的方式多一点启发,我们假设editor.on('paste')这种模式是系统级别的,只允许系统级插件有这种操作,而用户级插件不行,用户级插件只能通过系统级插件pasteTools暴露出来的register来注册,我们可以根据用户级插件的canHandle方法来让该插件只处理自己希望处理的那一部分。 类似的这种分离方法,也能更好地降低用户级插件对整个系统的影响

更新时间:
上一篇:CKEditor系列(二)事件系统是怎么实现的下一篇:到顶了

相关文章

CKEditor系列(二)事件系统是怎么实现的

CKEditor的事件系统的源代码在core/event.js里面 我们看看整个事件系统的实现过程 事件监听on CKEDITOR.event.prototype = ( function 阅读更多…

CKEditor系列(二)事件系统是怎么实现的

CKEditor的事件系统的源代码在core/event.js里面 我们看看整个事件系统的实现过程 事件监听on CKEDITOR.event.prototype = ( function 阅读更多…

CKEditor系列(一)CKEditor4项目怎么跑起来的

我们先看CKEditor的入口ckeditor.js,它里面有一部分是压缩版,压缩版部分对应的源码地址为src/core/ckeditor_base.js // src/core/ckedit 阅读更多…

CKEditor系列(一)CKEditor4项目怎么跑起来的

我们先看CKEditor的入口ckeditor.js,它里面有一部分是压缩版,压缩版部分对应的源码地址为src/core/ckeditor_base.js // src/core/ckedit 阅读更多…

关注道招网公众帐号
道招开发者二群