CKEditor系列(五)编辑器内容的设置和获取过程

2,073 阅读4分钟

原文转载自道招网的[《# CKEditor系列(五)编辑器内容的设置和获取过程》](www.daozhao.com/10324.html)

我们看一下CKEditor4的编辑器内容的设置和获取过程,也就是setData和getData过程。

我们在调用editor.setData的时候,调用的就是core/editor.js里面的setData方法。

// src/core/editor.js
setData: function( data, options, internal ) {
	var fireSnapshot = true,
		// Backward compatibility.
		callback = options,
		eventData;

	if ( options && typeof options == 'object' ) {
		internal = options.internal;
		callback = options.callback;
		fireSnapshot = !options.noSnapshot;
	}

	if ( !internal && fireSnapshot )
		this.fire( 'saveSnapshot' );

	if ( callback || !internal ) {
		this.once( 'dataReady', function( evt ) {
			if ( !internal && fireSnapshot )
				this.fire( 'saveSnapshot' );

			if ( callback )
				callback.call( evt.editor );
		} );
	}

	// Fire "setData" so data manipulation may happen.
	eventData = { dataValue: data };
	!internal && this.fire( 'setData', eventData );

	this._.data = eventData.dataValue;

	!internal && this.fire( 'afterSetData', eventData );
},

我们可以看到里面的set过程实际是分三步

  1. 判断是否需要saveSnapshot
  2. 判断是否需要触发setData事件
  3. 判断是否需要触发afterSetData事件

setData之saveSnapshot

saveSnapshot主要是方便撤销操作的

// src/plugins/undo.plugin.js
// Save snapshots before doing custom changes.
editor.on( 'saveSnapshot', function( evt ) {
	undoManager.save( evt.data && evt.data.contentOnly );
} );

setData之setData

我们接着看setData事件的处理

src/core/section.js
editor.on( 'setData', function() {
	// Invalidate locked selection when unloading DOM.
	// (https://dev.ckeditor.com/ticket/9521, https://dev.ckeditor.com/ticket/5217#comment:32 and https://dev.ckeditor.com/ticket/11500#comment:11)
	editor.unlockSelection();

	// Webkit's selection will mess up after the data loading.
	if ( CKEDITOR.env.webkit )
		clearSelection();
} );

我们可以看到,它做的工作主要是解锁选区,看来实际做工作的还不是setData啊,它算是一个setData的准备工作可能更合适些。

setData之afterSetData

// src/core/editable.js
this.attachListener( editor, 'afterSetData', function() {
	this.setData( editor.getData( 1 ) );
}, this );

没错,这里又有个一个setDatagetData。。。原来他们才是真正的setDatagetData啊。

// src/core/editable.js
/**
 * @see CKEDITOR.editor#setData
 */
setData: function( data, isSnapshot ) {
	if ( !isSnapshot )
		data = this.editor.dataProcessor.toHtml( data );

	this.setHtml( data );
	this.fixInitialSelection();

	// Editable is ready after first setData.
	if ( this.status == 'unloaded' )
		this.status = 'ready';

	this.editor.fire( 'dataReady' );
},

/**
 * @see CKEDITOR.editor#getData
 */
getData: function( isSnapshot ) {
	var data = this.getHtml();

	if ( !isSnapshot )
		data = this.editor.dataProcessor.toDataFormat( data );

	return data;
},

setHtmlgetHtml本质就是原生node的innerHTML,所以setDatagetData的过程其实就是 this.editor.dataProcessor.toHtmlthis.editor.dataProcessor.toDataFormat的过程,这个两个方法哪来的?它们都源自dataProcessor,它是在编辑器初始化的时候赋值的。

dataProcessor

// src/core/editor.js
// Various other core components that read editor configuration.
function initComponents( editor ) {
	// Documented in dataprocessor.js.
	editor.dataProcessor = new CKEDITOR.htmlDataProcessor( editor );

	// Set activeFilter directly to avoid firing event.
	editor.filter = editor.activeFilter = new CKEDITOR.filter( editor );

	loadSkin( editor );
}

dataProcessor的两个具体方法如下

// src/core/dataProcessor.js
toHtml: function( data, options, fixForBody, dontFilter ) {
	var editor = this.editor,
		context, filter, enterMode, protectedWhitespaces;

	// Typeof null == 'object', so check truthiness of options too.
	if ( options && typeof options == 'object' ) {
		context = options.context;
		fixForBody = options.fixForBody;
		dontFilter = options.dontFilter;
		filter = options.filter;
		enterMode = options.enterMode;
		protectedWhitespaces = options.protectedWhitespaces;
	}
	// Backward compatibility. Since CKEDITOR 4.3.0 every option was a separate argument.
	else {
		context = options;
	}

	// Fall back to the editable as context if not specified.
	if ( !context && context !== null )
		context = editor.editable().getName();

	return editor.fire( 'toHtml', {
		dataValue: data,
		context: context,
		fixForBody: fixForBody,
		dontFilter: dontFilter,
		filter: filter || editor.filter,
		enterMode: enterMode || editor.enterMode,
		protectedWhitespaces: protectedWhitespaces
	} ).dataValue;
},
toDataFormat: function( html, options ) {
	var context, filter, enterMode;

	// Do not shorten this to `options && options.xxx`, because
	// falsy `options` will be passed instead of undefined.
	if ( options ) {
		context = options.context;
		filter = options.filter;
		enterMode = options.enterMode;
	}

	// Fall back to the editable as context if not specified.
	if ( !context && context !== null )
		context = this.editor.editable().getName();

	return this.editor.fire( 'toDataFormat', {
		dataValue: html,
		filter: filter || this.editor.filter,
		context: context,
		enterMode: enterMode || this.editor.enterMode
	} ).dataValue;
},

这两个方法的具体实现被化成对两个(toHtmltoDataFormat)事件的处理逻辑了。

dataProcessor之toHtml

这两个事件有哪些回调呢,先看toHtml

// src/core/dataProcessor.js
editor.on( 'toHtml', function( evt ) {
	var evtData = evt.data,
	data = evtData.dataValue,
	fixBodyTag;

	// Before we start protecting markup, make sure there are no externally injected
	// protection keywords.
	data = removeReservedKeywords( data );

	// The source data is already HTML, but we need to clean
	// it up and apply the filter.
	data = protectSource( data, editor );

	// Protect content of textareas. (https://dev.ckeditor.com/ticket/9995)
	// Do this before protecting attributes to avoid breaking:
	// <textarea><img src="..." /></textarea>
	data = protectElements( data, protectTextareaRegex );

	// Before anything, we must protect the URL attributes as the
	// browser may changing them when setting the innerHTML later in
	// the code.
	data = protectAttributes( data );

	// Protect elements than can't be set inside a DIV. E.g. IE removes
	// style tags from innerHTML. (https://dev.ckeditor.com/ticket/3710)
	data = protectElements( data, protectElementsRegex );

	// Certain elements has problem to go through DOM operation, protect
	// them by prefixing 'cke' namespace. (https://dev.ckeditor.com/ticket/3591)
	data = protectElementsNames( data );

	// All none-IE browsers ignore self-closed custom elements,
	// protecting them into open-close. (https://dev.ckeditor.com/ticket/3591)
	data = protectSelfClosingElements( data );

	// Compensate one leading line break after <pre> open as browsers
	// eat it up. (https://dev.ckeditor.com/ticket/5789)
	data = protectPreFormatted( data );

	// There are attributes which may execute JavaScript code inside fixBin.
	// Encode them greedily. They will be unprotected right after getting HTML from fixBin. (https://dev.ckeditor.com/ticket/10)
	data = protectInsecureAttributes( data );

	var fixBin = evtData.context || editor.editable().getName(),
		isPre;

	// Old IEs loose formats when load html into <pre>.
	if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && fixBin == 'pre' ) {
		fixBin = 'div';
		data = '<pre>' + data + '</pre>';
		isPre = 1;
	}

	// Call the browser to help us fixing a possibly invalid HTML
	// structure.
	var el = editor.document.createElement( fixBin );
	// Add fake character to workaround IE comments bug. (https://dev.ckeditor.com/ticket/3801)
	el.setHtml( 'a' + data );
	data = el.getHtml().substr( 1 );

	// Restore shortly protected attribute names.
	data = data.replace( new RegExp( 'data-cke-' + CKEDITOR.rnd + '-', 'ig' ), '' );

	isPre && ( data = data.replace( /^<pre>|<\/pre>$/gi, '' ) );

	// Unprotect "some" of the protected elements at this point.
	data = unprotectElementNames( data );

	data = unprotectElements( data );

	// Restore the comments that have been protected, in this way they
	// can be properly filtered.
	data = unprotectRealComments( data );

	if ( evtData.fixForBody === false ) {
		fixBodyTag = false;
	} else {
		fixBodyTag = getFixBodyTag( evtData.enterMode, editor.config.autoParagraph );
	}

	// Now use our parser to make further fixes to the structure, as
	// well as apply the filter.
	data = CKEDITOR.htmlParser.fragment.fromHtml( data, evtData.context, fixBodyTag );

	// The empty root element needs to be fixed by adding 'p' or 'div' into it.
	// This avoids the need to create that element on the first focus (https://dev.ckeditor.com/ticket/12630).
	if ( fixBodyTag ) {
		fixEmptyRoot( data, fixBodyTag );
	}

	evtData.dataValue = data;
}, null, null, 5 );

// Filter incoming "data".
// Add element filter before htmlDataProcessor.dataFilter when purifying input data to correct html.
editor.on( 'toHtml', function( evt ) {
	if ( evt.data.filter.applyTo( evt.data.dataValue, true, evt.data.dontFilter, evt.data.enterMode ) )
		editor.fire( 'dataFiltered' );
}, null, null, 6 );

editor.on( 'toHtml', function( evt ) {
	evt.data.dataValue.filterChildren( that.dataFilter, true );
}, null, null, 10 );

editor.on( 'toHtml', function( evt ) {
	var evtData = evt.data,
		data = evtData.dataValue,
		writer = new CKEDITOR.htmlParser.basicWriter();

	data.writeChildrenHtml( writer );
	data = writer.getHtml( true );

	// Protect the real comments again.
	evtData.dataValue = protectRealComments( data );
}, null, null, 15 );

我们可以看到这些回调里面最多的几个单词就是protectfilter,它们主要也是做这些工作。

dataProcessor之toDataFormat

再看看toDataFormat的回调

// src/core/dataProcessor.js
editor.on( 'toDataFormat', function( evt ) {
	var data = evt.data.dataValue;

	// https://dev.ckeditor.com/ticket/10854 - we need to strip leading blockless <br> which FF adds
	// automatically when editable contains only non-editable content.
	// We do that for every browser (so it's a constant behavior) and
	// not in BR mode, in which chance of valid leading blockless <br> is higher.
	if ( evt.data.enterMode != CKEDITOR.ENTER_BR )
		data = data.replace( /^<br *\/?>/i, '' );

	evt.data.dataValue = CKEDITOR.htmlParser.fragment.fromHtml(
		data, evt.data.context, getFixBodyTag( evt.data.enterMode, editor.config.autoParagraph ) );
}, null, null, 5 );

editor.on( 'toDataFormat', function( evt ) {
	evt.data.dataValue.filterChildren( that.htmlFilter, true );
}, null, null, 10 );

// Transform outcoming "data".
// Add element filter after htmlDataProcessor.htmlFilter when preparing output data HTML.
editor.on( 'toDataFormat', function( evt ) {
	evt.data.filter.applyTo( evt.data.dataValue, false, true );
}, null, null, 11 );

editor.on( 'toDataFormat', function( evt ) {
	var data = evt.data.dataValue,
		writer = that.writer;

	writer.reset();
	data.writeChildrenHtml( writer );
	data = writer.getHtml( true );

	// Restore those non-HTML protected source. (https://dev.ckeditor.com/ticket/4475,https://dev.ckeditor.com/ticket/4880)
	data = unprotectRealComments( data );
	data = unprotectSource( data, editor );

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

总结

编辑器内容的设置和获取表面上是简单只是调用一个方法就完成了,但是其实内部的流程还是很长的,大致分为:

  1. 消息告知saveSnapshot
  2. 准备工作setData
  3. 处理流程dataProcessor
  4. 发送事件 toHtml
  5. 系统事件(优先级小于10)处理 protectfilter
  6. 系统事件(优先级大于10)处理,进行最后的兜底(插入或获取)逻辑 CKEDITOR.htmlParser.basicWriter