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 );
,对应的abort
和error
也要触发,避免因图片失败,导致其它回调都没机会执行了。
该回调会在下一轮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
增加dataTransfer
、dataValue
(如果已经被其它插件设置了就直接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 );
这个主要是根据不同的type
、trueType
来对数据进行一些过滤操作
插入粘贴数据
粘贴的数据总得进入到编辑器吧,这就靠它了。
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文件,里面有个对工具栏增加粘贴按钮,加上paste
Command的操作
{
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
方法来让该插件只处理自己希望处理的那一部分。
类似的这种分离方法,也能更好地降低用户级插件对整个系统的影响
- 分类:
- Web前端
相关文章
CKEditor系列(四)支持动态多语言i18n
多语言文件结构 先看下CKEditor4的多语言文件长什么样子 //src/lang/zh-cn.js CKEDITOR.lang[ 'zh-cn' ] = { 阅读更多…
富文本编辑器CKEditor4迁移方案
之前写过 《富文本编辑器wangEditor迁移CKEditor前后效果对比》 ,结合大家的反馈后进行了调整。 增加了具体案例的展示CKEditor插件和事件系统,重新整理成迁移方案。 一、背景 阅读更多…
CKEditor系列(六)改造原编辑器默认样式dom结构效果对比
熟悉的朋友应该知道之前是用的wangEditor,近期才迁移到CKEditor,很早的时候项目就支持一个叫“默认样式”的功能,需求就是部分BU希望能够统一邮件对外发送的样式,比如统一使用“宋体,黑色 阅读更多…
CKEditor系列(一)CKEditor4项目怎么跑起来的
我们先看CKEditor的入口ckeditor.js,它里面有一部分是压缩版,压缩版部分对应的源码地址为src/core/ckeditor_base.js // src/core/ckedit 阅读更多…
CKEditor系列(七)编辑器工具栏根据宽度自动折叠
刚才看了看上一篇写CKEditor的文章是在今年的一月份,现在轮到我们的设计师对编辑器下手了。我们回顾下现在的编辑器长什么样子。 需求 我们客户端默认窗口尺寸下,会出现排,并且第二排 阅读更多…
CKEditor系列(五)编辑器内容的设置和获取过程
我们看一下CKEditor4的编辑器内容的设置和获取过程,也就是setData和getData过程。 我们在调用 editor.setData 的时候,调用的就是 core/editor.js 阅读更多…