做Web开发,尤其是企业应用这块的开发者,最终都避免不了需要用到Html Editor。这一块我之前用的FCKEditor 2.4,现在FCKEditor已经不存在了,取而代之的是CKEditor。研究了一下,发现CKEditor的内部结构确实比FCKEditor时代有了很大改善(不过仍然还有很大的改进余地),之前FCKEditor别扭的配置方式在CKEditor里面被JSON全面取代了。下面是我这两天的研究成果,考虑到研究过程中在网上搜索到的这方面的资料还比较少,所以在这里记录一下,希望能帮到同样需要的朋友。
CKEditor版本:我研究的CKEditor的版本是3.1,也是目前的最新版本,这个版本我发现官网上只提供了php版。下载地址:http://ckeditor.com/download。
问题:
1.默认的CKEditor没有上传功能,如果要上传文件/图片需要CKFinder配合。而我的应用环境是ASP.net,即使安装了CKFinder也没法配合CKEditor使用。
2.CKEditor不提供只读功能,它只提供了一个Preview按钮,点击以后是弹出一个新窗口,窗口内容是编辑器的内容。这不是我想要的Preview效果。
目标:
1.默认的CKEditor的插入图片功能是会弹出一个对话框,要求输入图片的URL地址。我希望能在输入URL地址的文本框边上放一个Upload按钮,能够上传本地的图片到服务器,然后URL内自动获取图片在服务器上的访问URL。
2.在CKEditor工具栏上添加一个Readonly按钮,点击以后实现Preview功能--所有工具栏按钮不可用,内容不可编辑。
3.修改不要涉及CKEditor源代码的修改,方便以后CKEditor升级。
实现:
1.代码【1】:
//////////////////////////////////////////////////////////////////////////////////////////////////////////
function addUploadButton(editor){
CKEDITOR.on('dialogDefinition', function( ev ){
var dialogName = ev.data.name;
var dialogDefinition = ev.data.definition;
if ( dialogName == 'image' ){
var infoTab = dialogDefinition.getContents( 'info' );
infoTab.add({
type : 'button',
id : 'upload_image',
align : 'center',
label : 'upload',
onClick : function( evt ){
var thisDialog = this.getDialog();
var txtUrlObj = thisDialog.getContentElement('info', 'txtUrl');
var txtUrlId = txtUrlObj.getInputElement().$.id;
addNestedImage(txtUrlId);
}
}, 'browse'); //place front of the browser button
}
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
说明:
1.dialogDefinition是一个事件,通过对这一事件编程可以达到不修改源代码的情况下修改CKEditor的对话框。每个CKEditor的对话框都有一个名字。点击插入Image按钮后弹出的对话框的名字是image。
2.每个对话框由多个选项卡组成,通过getContents的方法可以定位选项卡。填写image Url的文本框在info选项卡内。
3.通过add方法可以为选项卡添加新的元素。add方法有2个参数,第一个是JSON格式的元素描述,第二个参数是新元素的插入位置,没有的话新元素就添加到选项卡最后。browse是image url文本框旁边默认的按钮,通常是配置了CKFinder以后才能看到。我因为没有安装CKFinder,所以看不到这个按钮,但不妨碍我借用它给我的Upload按钮定位。
4.Upload按钮的格式定义。不同的对话框元素有不同的属性。我这里是参考CKEditor的源码里其他按钮的格式定义出来的。type是元素的类型,id是标识符,要说明的是这个id不是最终的HTML元素的id,而是给CKEditor自己内部使用的id(通过getContentElement()方法,见后面)。label是按钮上的文字。onClick就是点击按钮后执行的动作。
5.我的想法是点击按钮后弹出一个新窗口,在新窗口中完成文件/图片上传功能。上传结束时返回图片的URL,然后把这个URL填到image URL文本框里。addNestedImage(txtUrlId)就是做的这件事。参数txtUrlId就是image URL的文本框的id(HTML元素的id)。通过
var thisDialog = this.getDialog();
var txtUrlObj = thisDialog.getContentElement('info', 'txtUrl');
var txtUrlId = txtUrlObj.getInputElement().$.id;
这三行代码可以实现JSON'id和HTML'id的转换。txtUrl就是image URL文本框的JSON id。这是看源码看来的。
6.addNestedImage的内容和CKEditor基本无关,代码就不放这儿了,基本思路是:接受一个目标参数theElementId,然后用window.open打开一个新的窗口,类似.../UploadImage.aspx?return_id=theElementId。UploadImage.aspx就是一个普通的上传页面,aspx在这块做的非常简单了,代码就不写了。需要说明的是上传结束后,需要执行一段js代码,内容是将上传的图片在服务器上的URL反写到theElementId指定的元素中。代码大致如下:
var tmpObj = window.opener.document.getElementById("<%=URL参数return_id%>");
if (tmpObj != null){
var tmpImageUrl = "<%=图片在server上的URL%>";
tmpObj.value = tmpImageUrl;
tmpObj.setAttribute("value", tmpImageUrl);
tmpObj.fireEvent("onchange");
}
window.close();
最后用window.close()关掉窗口就好了。
2.代码【2】:
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Temporary workaround for providing editor 'read-only' toggling functionality.
( function(){
var cancelEvent = function( evt ){
evt.cancel();
};
CKEDITOR.editor.prototype.readOnly = function( isReadOnly ){
// Turn off contentEditable
/*
if ( this.document ){
this.document.$.body.disabled = isReadOnly;
if ( CKEDITOR.env.ie ){
this.document.$.body.contentEditable = !isReadOnly;
}else{
this.document.$.designMode = (isReadOnly ? "off" : "on");
}
}
*/
// Prevent key handling.
this[ isReadOnly ? 'on' : 'removeListener' ]( 'key', cancelEvent, null, null, 0 );
this[ isReadOnly ? 'on' : 'removeListener' ]( 'selectionChange', cancelEvent, null, null, 0 );
// Disable all commands in wysiwyg mode.
var command,
commands = this._.commands,
mode = this.mode;
for ( var name in commands ){
if (name != "ReadonlyCmd"){
command = commands[ name ];
if ( isReadOnly ) {
command.disable();
}else{
command[ command.modes[ mode ] ? 'enable' : 'disable' ]();
}
this[ isReadOnly ? 'on' : 'removeListener' ]( 'state', cancelEvent, null, null, 0 );
}
}
var i,j,k;
var toolbars = this.toolbox.toolbars;
for ( i = 0; i < toolbars.length; i++ ){
var toolbarItems = toolbars[i].items;
for ( j = 0; j < toolbarItems.length; j++ ){
var combo = toolbarItems[j].combo;
if ( combo ){
combo.setState( isReadOnly ? CKEDITOR.TRISTATE_DISABLED : CKEDITOR.TRISTATE_OFF );
}
var button = toolbarItems[j].button;
if ( button && button.createPanel ){
button.setState( isReadOnly ? CKEDITOR.TRISTATE_DISABLED : CKEDITOR.TRISTATE_OFF );
}
}
}
}
} )();
//代码【3】:
function addReadonlyButton(editor, theMode){
editor.on('pluginsLoaded', function( ev ){
var savedState = CKEDITOR.TRISTATE_OFF;
var i,j,k;
editor.addCommand( 'ReadonlyCmd', {
exec : function (e){
switch (this.state){
case CKEDITOR.TRISTATE_OFF :
editor.readOnly(true);
break;
case CKEDITOR.TRISTATE_ON :
editor.readOnly(false);
break;
}
this.toggleState();
savedState = this.state;
},
canUndo : false
});
editor.ui.addButton( 'Readonly', {
label : 'Readonly',
command : 'ReadonlyCmd'
});
editor.on( 'mode', function() {
editor.getCommand( 'ReadonlyCmd' ).setState( savedState );
}, null, null, 100 );
});
setReadonlyButtonStyle();
}
function setReadonlyButtonStyle(){
var cssText = ''
+ '.cke_button_ReadonlyCmd .cke_icon{'
+ ' display: none !important;'
+ '}'
+ '.cke_button_ReadonlyCmd .cke_label{'
+ ' display: inline !important;'
+ '}';
importStyle(cssText);
}
function importStyle(theCssText){
var styleElement = document.createElement('style');
styleElement.setAttribute("type", "text/css");
if (styleElement.styleSheet) { //for IE
styleElement.styleSheet.cssText = theCssText;
}else{ //for FF
var textNode = document.createTextNode(theCssText);
styleElement.appendChild(textNode);
}
var headElement = document.getElementsByTagName('head')[0];
headElement.appendChild(styleElement);
}
//代码【4】:
function initEditor(theName, theMode, theWidth, theHeight){
var editor = CKEDITOR.replace(theName, {
language : 'en',
skin : 'office2003',
startupFocus : true,
toolbar : [ //remove Save, NewPage, Form and input elements, add Readonly
['Source','-','Readonly','-','Templates'],
['Cut','Copy','Paste','PasteText','PasteFromWord','-','Print', 'SpellChecker', 'Scayt'],
['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'],
'/',
['Bold','Italic','Underline','Strike','-','Subscript','Superscript'],
['NumberedList','BulletedList','-','Outdent','Indent','Blockquote'],
['JustifyLeft','JustifyCenter','JustifyRight','JustifyBlock'],
['Link','Unlink','Image','Table','HorizontalRule','Smiley','SpecialChar','PageBreak'],
'/',
['Styles','Format','Font','FontSize'],
['TextColor','BGColor'],
['Maximize', 'ShowBlocks','-','About']
],
width : theWidth,
height : theHeight
});
addUploadButton(editor);
addReadonlyButton(editor);
if (theMode == "VIEW"){
setTimeout(function(){
editor.execCommand('ReadonlyCmd'); //click readonly
}, 500);
}
setTimeout(function(){
var tmpObj = document.getElementById("cke_" + theName);
var tmpInputs = tmpObj.getElementsByTagName("input");
if (tmpInputs.length > 0){
tmpInputs[0].style.display = "none";
}
}, 500);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
说明:
1.代码【2】是为CKEditor扩展readonly的功能。代码【3】是为CKEditor添加Readonly按钮,代码【2】是将readonly按钮显示在工具栏上,同时包括其他的配置。
2.代码【4】是一个封装函数,方便以后移植到其他的Html Editor。参数theName是数据源的id,theMode指明Html Editor的状态,"EDIT"是编辑状态,"VIEW"是只读状态。theWidth和theHeight是Html Editor的宽和高。CKEDITOR.replace()就不说了,用过CKEditor的都知道。关键是toolbar : [...,'Readonly'...这就是将Readonly按钮显示在工具栏上。而Readonly按钮的定义通过代码【3】完成。
3.CKEDITOR.replace()返回CKEditor的实例editor,然后通过调用前面定义的addUploadButton()和addReadonlyButton()完成给CKEditor非侵入的修改对话框和工具栏。
4.如果theMode是"VIEW",则手动调用一次Redonly按钮,相当于按一下Readonly按钮。
5.最后一段是将CKEditor的一个文本框隐藏起来。每个CKEditor编辑器末尾都有一个input元素显示在页面上。我不知道CKEditor为什么要把这个input元素显示出来,看着真别扭。不管它,用户看不见就好。
6.代码【3】就是完成Readonly按钮的定义。每个CKEditor的按钮其实都至少包括2部分内容,一部分是工具栏上的按钮,这是用户看得见的部分,另一部分是点击这个按钮后CKEditor执行的动作。在CKEditor里面是一个command对象。通过pluginsLoaded这个事件可以为CKEditor添加新的button和command。添加buttong通过editor.ui.addButton进行,添加command通过editor.addCommand进行。先定义Command,然后定义button的时候通过指定button的JSON command属性将command和button关联起来。这里command和button没有必然联系,完全可以定义一个新的button,然后执行现有的command。
7.editor.on( 'mode', function(){...}, null, null, 100 )这个是用来实现Readonly按钮的有效和非有效状态。通常CKEditor如果是EDIT状态,那么Readonly是可用的,如果CKEditor已经是VIEW状态了,那么我希望Readonly就是已经按下去的样子。mode事件就是做这事的。
8.setReadonlyButtonStyle()和importStyle()是定义Readonly按钮的外观。看CKEditor的Sample看来的,没什么好说的。不定义的话按钮看不到。本来我想给Readonly按钮加个图标,就象Source一样,但是失败了。有知道怎么弄的兄弟请指导一下。
9.代码【2】是定义点击Readonly按钮以后执行的editor.readOnly(),这是对CKEditor的扩展。这一段我开始一直搞不定,只到从Google上搜到http://cksource.com/forums/viewtopic.php?f=11&t=15659这篇文章。里面garry.yao兄对我非常有帮助。这里感谢一下。但是garry.yao兄的代码只实现了普通按钮的禁用,点击Readonly以后没有禁用Style,Format,Font,Color这些按钮。我添加了一些代码,完成这些功能。
10.将代码【1】【2】【3】【4】合并放到一个js文件中,然后在页面onload的时候执行initEditor()就可以了。
Tue, 01/26/2010 - 10:55
#1
Re: 为CKEditor添加Readonly按钮和Upload Image按钮
Thanks in advance
Re: 为CKEditor添加Readonly按钮和Upload Image按钮