CKEditor系列(一)CKEditor4项目怎么跑起来的
CKEditor系列(一)CKEditor4项目怎么跑起来的
我们先看CKEditor的入口ckeditor.js,它里面有一部分是压缩版,压缩版部分对应的源码地址为src/core/ckeditor_base.js
// src/core/ckeditor_base.js
if ( !window.CKEDITOR ) {
window.CKEDITOR = ( function() {
var basePathSrcPattern = /(^|.*[\\\/])ckeditor\.js(?:\?.*|;.*)?$/i;
var CKEDITOR = {
_: {
pending: [],
basePathSrcPattern: basePathSrcPattern
},
status: 'unloaded',
basePath: ( function() {})(),
// Find out the editor directory path, based on its <script> tag.
var path = window.CKEDITOR_BASEPATH || '';
return path;
} )(),
domReady: ( function() {
// Based on the original jQuery code (available under the MIT license, see LICENSE.md).
var callbacks = [];
return function( fn ) {
callbacks.push( fn );
};
} )()
};
return CKEDITOR;
} )();
}
我对里面的代码进行了节选。
... // 上述src/core/ckeditor_base.js的压缩版
if ( CKEDITOR.loader )
CKEDITOR.loader.load( 'ckeditor' );
else {
// Set the script name to be loaded by the loader.
CKEDITOR._autoLoad = 'ckeditor';
// Include the loader script.
if ( document.body && ( !document.readyState || document.readyState == 'complete' ) ) {
var script = document.createElement( 'script' );
script.type = 'text/javascript';
script.src = CKEDITOR.getUrl( 'core/loader.js' );
document.body.appendChild( script );
} else {
document.write( '<script type="text/javascript" src="' + CKEDITOR.getUrl( 'core/loader.js' ) + '"></script>' );
}
}
首次加载的时候其实是没有CKEDITOR.loader
,所以进入上面的判断的else
部分,会开始加载 core/loader.js
,这是必要的准备工作里面的最后一环。
// core/loader.js
CKEDITOR.loader = ( function() {
// Table of script names and their dependencies.
var scripts = {
'_bootstrap': [
'config', 'creators/inline', 'creators/themedui', 'editable', 'ckeditor', 'plugins',
'scriptloader', 'style', 'tools', 'promise', 'selection/optimization', 'tools/color',
// The following are entries that we want to force loading at the end to avoid dependence recursion.
'dom/comment', 'dom/elementpath', 'dom/text', 'dom/rangelist', 'skin'
],
'ckeditor': [
'ckeditor_basic', 'log', 'dom', 'dtd', 'dom/document', 'dom/element', 'dom/iterator', 'editor', 'event',
'htmldataprocessor', 'htmlparser', 'htmlparser/element', 'htmlparser/fragment', 'htmlparser/filter',
'htmlparser/basicwriter', 'template', 'tools'
],
'ckeditor_base': [],
'ckeditor_basic': [ 'editor_basic', 'env', 'event' ],
load: function( scriptName, defer ) {
// Check if the script has already been loaded.
if ( ( 's:' + scriptName ) in this.loadedScripts )
return;
// Get the script dependencies list.
var dependencies = scripts[ scriptName ];
if ( !dependencies )
throw 'The script name"' + scriptName + '" is not defined.';
// Mark the script as loaded, even before really loading it, to
// avoid cross references recursion.
// Prepend script name with 's:' to avoid conflict with Array's methods.
this.loadedScripts[ 's:' + scriptName ] = true;
// Load all dependencies first.
for ( var i = 0; i < dependencies.length; i++ )
this.load( dependencies[ i ], true );
var scriptSrc = getUrl( 'core/' + scriptName + '.js' );
// Append the <script> element to the DOM.
// If the page is fully loaded, we can't use document.write
// but if the script is run while the body is loading then it's safe to use it
// Unfortunately, Firefox <3.6 doesn't support document.readyState, so it won't get this improvement
if ( document.body && ( !document.readyState || document.readyState == 'complete' ) ) {
pendingLoad.push( scriptName );
if ( !defer )
this.loadPending();
} else {
// Append this script to the list of loaded scripts.
this.loadedScripts.push( scriptName );
document.write( '<script src="' + scriptSrc + '" type="text/javascript"><\/script>' );
}
}
};
} )();
我们可以看到,这里面定义需要加载的js脚本的名字和它对应的依赖。
现在就通过CKEDITOR.loader.load( 'ckeditor' )
来加载ckeditor(当然,根据上面的代码可以看出,core/ckeditor
也有依赖,依赖加载完了才能真的轮到它),这一切就正式开始了。
在core/ckeditor
里面还会加载core/_bootstrap
,我们可以看到CKEditor的代码基本都是按照自执行函数的写法写的。
接下来我们看看具体在目前主流框架里面的启动部分,以React为例
在react4-react
的源码里面我们可以看到。
// src/useCKEditor.ts
const initEditor = ( CKEDITOR: CKEditorNamespace ) => {
const isInline = typeRef.current === 'inline';
const isReadOnly = configRef.current.readOnly;
/**
* Dispatches `beforeLoad` event.
*/
if ( subscribeToRef.current.indexOf( 'beforeLoad' ) !== -1 ) {
dispatchEventRef.current?.( {
type: CKEditorEventAction.beforeLoad,
payload: CKEDITOR
} );
}
const editor = CKEDITOR[ isInline ? 'inline' : 'replace' ](
element,
configRef.current
);
}
我们一般使用的默认的编辑器模式即classic,所以上面我们调用CKEDITOR['replace'](element,configRef.current)
来开始创建编辑器实例
// src/core/creator/themeui.js
CKEDITOR.replace = function( element, config ) {
return createInstance( element, config, null, CKEDITOR.ELEMENT_MODE_REPLACE );
};
function createInstance( element, config, data, mode ) {
element = CKEDITOR.editor._getEditorElement( element );
if ( !element ) {
return null;
}
// (#4461)
if ( CKEDITOR.editor.shouldDelayEditorCreation( element, config ) ) {
CKEDITOR.editor.initializeDelayedEditorCreation( element, config, 'replace' );
return null;
}
// Create the editor instance.
var editor = new CKEDITOR.editor( config, element, mode );
if ( mode == CKEDITOR.ELEMENT_MODE_REPLACE ) {
// Do not replace the textarea right now, just hide it. The effective
// replacement will be done later in the editor creation lifecycle.
element.setStyle( 'visibility', 'hidden' );
// https://dev.ckeditor.com/ticket/8031 Remember if textarea was required and remove the attribute.
editor._.required = element.hasAttribute( 'required' );
element.removeAttribute( 'required' );
}
data && editor.setData( data, null, true );
// Once the editor is loaded, start the UI.
editor.on( 'loaded', function() {
if ( editor.isDestroyed() || editor.isDetached() ) {
return;
}
loadTheme( editor );
if ( mode == CKEDITOR.ELEMENT_MODE_REPLACE && editor.config.autoUpdateElement && element.$.form )
editor._attachToForm();
editor.setMode( editor.config.startupMode, function() {
// Clean on startup.
editor.resetDirty();
// Editor is completely loaded for interaction.
editor.status = 'ready';
editor.fireOnce( 'instanceReady' );
CKEDITOR.fire( 'instanceReady', null, editor );
} );
} );
editor.on( 'destroy', destroy );
return editor;
}
终于可以看看Editor
构造函数了
// core/editor.js
function Editor( instanceConfig, element, mode ) {
// Call the CKEDITOR.event constructor to initialize this instance.
CKEDITOR.event.call( this );
// Make a clone of the config object, to avoid having it touched by our code. (https://dev.ckeditor.com/ticket/9636)
instanceConfig = instanceConfig && CKEDITOR.tools.clone( instanceConfig );
// Declare the private namespace.
this._ = {};
this.commands = {};
this.templates = {};
this.name = this.name || genEditorName();
/**
* A unique random string assigned to each editor instance on the page.
*
* @readonly
* @property {String}
*/
this.id = CKEDITOR.tools.getNextId();
this.status = 'unloaded';
this.config = CKEDITOR.tools.prototypedCopy( CKEDITOR.config );
/**
* The namespace containing UI features related to this editor instance.
*
* @readonly
* @property {CKEDITOR.ui}
*/
this.ui = new CKEDITOR.ui( this );
this.focusManager = new CKEDITOR.focusManager( this );
/**
* Controls keystroke typing in this editor instance.
*
* @readonly
* @property {CKEDITOR.keystrokeHandler}
*/
this.keystrokeHandler = new CKEDITOR.keystrokeHandler( this );
// Make the editor update its command states on mode change.
this.on( 'readOnly', updateCommands );
this.on( 'selectionChange', function( evt ) {
updateCommandsContext( this, evt.data.path );
} );
this.on( 'activeFilterChange', function() {
updateCommandsContext( this, this.elementPath(), true );
} );
this.on( 'mode', updateCommands );
// Optimize selection starting/ending on element boundaries (#3175).
CKEDITOR.dom.selection.setupEditorOptimization( this );
// Handle startup focus.
this.on( 'instanceReady', function() {
if ( this.config.startupFocus ) {
if ( this.config.startupFocus === 'end' ) {
var range = this.createRange();
range.selectNodeContents( this.editable() );
range.shrink( CKEDITOR.SHRINK_ELEMENT, true );
range.collapse();
this.getSelection().selectRanges( [ range ] );
}
this.focus();
}
} );
CKEDITOR.fire( 'instanceCreated', null, this );
// Add this new editor to the CKEDITOR.instances collections.
CKEDITOR.add( this );
// Return the editor instance immediately to enable early stage event registrations.
CKEDITOR.tools.setTimeout( function() {
if ( !this.isDestroyed() && !this.isDetached() ) {
initConfig( this, instanceConfig );
}
}, 0, this );
}
我们可以看到最后面有initConfig
,根据配置开始生成出我们的编辑器了。
接下来的路径大致就如下了,里面还会穿插一些对当且阶段完成了的通知消息:
initConfig
onConfigLoaded
initComponents
loadSkin
loadLang
preloadStylesSet
loadPlugins
终于到我们的加载插件部分了。。。 插件加载完了,就可以通知宣布这个实例已经加载好了
// core/editor.js
editor.status = 'loaded';
editor.fireOnce( 'loaded' );
此前不少插件就已经监听loaded
事件了,比如
// core/creator/themeui.js
editor.on( 'loaded', function() {
if ( editor.isDestroyed() || editor.isDetached() ) {
return;
}
loadTheme( editor );
}
在loadTheme
方法中会触发uiSpace
消息,而我们工具栏(core/toolbar.js
)就是在监听到这个消息后,才开始对工具栏各操作按钮的生成的。
// plugins/toolbar/plugin.js
editor.on( 'uiSpace', function( event ) {
for ( var r = 0; r < toolbarLength; r++ ) {
// Create all items defined for this toolbar.
for ( var i = 0; i < items.length; i++ ) {
function addItem( item ) { // jshint ignore:line
var itemObj = item.render( editor, output );
index = toolbarObj.items.push( itemObj ) - 1;
}
addItem( item );
}
}
}
记住上面的item.render
,我们的工具栏就是调用各插件的render
来生成的工具栏。
好,后续部分下次再写了。
- 分类:
- Web前端
相关文章
CKEditor富文本编辑器职责分离
背景 CKEditor富文本 编辑器 (生产版本1.1.1及以前)里面包含富文本基础插件及当前最新的邮件特定的业务插件(签名、快捷回复、邀评、默认样式、选择颜色、插入图片、粘贴模式) O端 阅读更多…
CKEditor系列(七)编辑器工具栏根据宽度自动折叠
刚才看了看上一篇写CKEditor的文章是在今年的一月份,现在轮到我们的设计师对编辑器下手了。我们回顾下现在的编辑器长什么样子。 需求 我们客户端默认窗口尺寸下,会出现排,并且第二排 阅读更多…
WordPress项目做vue的后台管理系统
鉴于最近很多朋友想利用手上的wordpress博客来做后台系统,前端页面想使用自己更加上手的vue或者react来开发。 这个想法和我当时差不多啊,我当时就是不想继续使用自己的WordPress做前 阅读更多…
CKEditor系列(五)编辑器内容的设置和获取过程
我们看一下CKEditor4的编辑器内容的设置和获取过程,也就是setData和getData过程。 我们在调用 editor.setData 的时候,调用的就是 core/editor.js 阅读更多…
CKEditor系列(六)改造原编辑器默认样式dom结构效果对比
熟悉的朋友应该知道之前是用的wangEditor,近期才迁移到CKEditor,很早的时候项目就支持一个叫“默认样式”的功能,需求就是部分BU希望能够统一邮件对外发送的样式,比如统一使用“宋体,黑色 阅读更多…
记录CKEditor4删除文本引起文本分割而升级版本的经历
背景 前段时间对接了一个electron壳提供功能————拼写检查,也就是在输入的英文有问题的时候,给予红色波浪线提示,邮件的时候能出现候选词,选择候选词后进行替换。 在功能上线当天上午产品 阅读更多…