玩了tiny_mce在线编辑器好几个星期,今天终于差不多把所有的功能都给完成了,确切的说是把编辑器的插件功能完美的整合在我的博客里面,解决一些小的bug,这还得意于它本身是开源免费的,这里我实现的功能主要有:

  1. 修改图片和多媒体文件上传和浏览功能;
  2. 增加signature个性签名(关联博客)和insertcode插入代码(整合CodeHighlighter代码高亮显示)功能插件;
  3. 修改编辑器内按下Ctrl+S键save保存插件功能,使之支持Postback到服务器端并触发OnSave事件。
  4. 修正编辑器内字体过小、设置编辑器不会自动移除div元素节点的等问题。

起初我引用tiny_mce编辑器都是直接嵌入的脚本的,摸索了一番待完善所有功能后,当然就要把它做成.net的自定义控件了,方便每一个页面调用,下面我就结合在做自定义控件的时候说一下Tinymce编辑器。

试试我的TinyMCE在线编辑器

先上自定义控件源代码:

TinyMce服务器自定义控件代码

首先做一个自定义控件需要考虑的继承类,是Control还是WebControl?WebControl本身是继承自Control的类,它跟Control类相比拥有Web服务器控件的更多属性,因此我这里选择继承自WebControl类,其实我本来还试过直接继承自HtmlTextArea类的,熟悉tinymce编辑器的朋友知道,Tinymce编辑器初始化的时候只需要简单的指定一个textarea元素ID就有了,而HtmlTextArea类就是用于产生textarea的html服务器端控件,这样那些Cols、Rows、Style等属性也不需要重新定义,简单的Tinymce编辑器初始化代码如下:

<textarea id="txtContent" name="txtContent" rows="20" cols="100" style="height:200px"></textarea>
<script type="text/javascript" src="../tiny_mce/tiny_mce.js"></script>
<script type="text/javascript" >
tinyMCE.init({
mode :
"exact",
elements :
"txtContent",
theme :
"simple",
language :
"zh",
content_css :
"/tiny_mce/css/content.css"
});
</script>

如果是继承自HtmlTextArea类的话,首先可以直接产生一个textarea控件,并可以设置Value等属性控制输出值,那么再输出tinymce编辑器主脚本路径和设置和elements为textarea客户端产生的id即可,好象可以行得通,我试着写个测试也通过了,不过我发现继承自Control类(HtmlTextArea类隐式继承Control)在VS切换到设计视图会有问题,在描绘控件控件时候会提示一些错误,有人说是VS版本的问题,我也在网上搜索一些相关资料,尝试了好几种方法都未解决,不过这个问题不会影响到程序的正常运行,喜欢追求完美的我便因为这个小问题毅然放弃了继承Control类。。。

选择好了继承自WebControl类后就开始Copy进TinyMce编辑器的一些属性,由于WebControl类默认不会产生textarea,所以我们需要在初始化的时候实例化一个HtmlTextArea类,以产生textarea控件,并且重写掉父类的Width、Height、CssClass、ClientID等属性,使之当设置这些属性的时候直接关联到textarea控件属性。重写Render输出html的方法以实现自定义输出,这里我注销掉了WebControl父类的Render方法,因为它默认会产生输出一个span标签,而这里不需要。后面接着调用HtmlTextArea控件的Render方法生成textarea,当在VS的ASPX设计视图里Context上下文为空则返回,所以我这里自定义控件在VS设计视图里面看到的就是一个textarea控件。最关键的后面产生JavaScript代码,在输出引用tinymce脚本主文件的时候做了一个判断,因为页面内可能包含了多个Tinymce编辑器控件,但tinymce的脚本主文件只需要引用一次即可,避免重复在页面引用输出,这里我是用Items暂存变量包保存页面的Tinymce编辑器个数,它在整个页面生命周期内有效,之前这个我是在TinyMce自定义控件类初始化的做判断,发现保存在Items暂存变量保存的值有问题,不知道是否为还未传递HttpContext对象的问题。接下来就是tinymce编辑器初试化一些参数的设置问题了,我类属性里面设置了一些默认值,这些都是我摸索着配置出来的默认设置,这里值得一提的是valid_elements属性,当我们在编辑器html源代码里面写一个空div但插入后却被莫名奇妙的被移除设置此属性即可,它主要是验证元素及拥有属性,默认设置的一些元素都无onclick等属性,所以你没有设置它再编辑器内怎么也给不了button按钮一个onclick事件,或插入不了一个空div元素,因为它们不在默认的valid_elements范围之内,不需要使用某些元素比如script脚本标记我们也可以通过设置valid_elements以过滤掉一些不需要使用的标签。

输出tinymce初始化脚本的时候这里还有一个重要的参数onserversave,是的,它就是触发TinyMce自定义控件的OnSave事件参数,tinymce编辑器本身是没有onserversave这个配置属性的,还好我在客户端使用js通过ed.getParam('onserversave')能获取在服务器端输出的值,那么这个onserversave值到底是什么呢?首先我们这个我的这个TinyMce自定义控件类实现IPostBackEventHandler了接口,它使服务器控件能够处理将窗体发送到服务器时引发的事件,也就是在tinymce编辑器内按Ctrl+S键时Postback到服务器,并响应TinyMce自定义控件OnSave事件,需要实现RaisePostBackEvent方法,以处理响应事件,并传递eventArgument事件参数,我这里只是做了一个简单的判断,如果TinyMce自定义控件在服务器端设置了OnSave处理事件,则回调该事件,在一些比较复杂的复合控件里面我们可以通过eventArgument参数不同响应不同事件,比如我们在GridView控件里经常能看到编辑按钮是__doPostBack('GridView1','Edit$0')等字样,它所传递响应的就是GridView1控件第0行编辑按钮事件,服务器端获取的eventArgument就是这里的Edit$0,而__doPostBack函数第一个参数__EVENTTARGET是触发服务器控件的UniqueID(不是控件ClientID,相当于Name),说到这里有必要讲一下Asp.Net服务器控件__doPostBack回发事件处理机智,这个还是得从客户端submit说起,如果页面只有一些button服务器控件,这些按钮是直接生成sumbit的,所以这是一个再简单不过的提交表单动作,不需要借助__doPostBack回发函数,所以页面也不会产生任何__doPostBack函数的JavaScript代码,在点击sumbit按钮的时候form会把该按钮的name和value作为健值对post到服务器端,所以在服务器端就能获得触发postback的sumbit按钮name(即UniqueID),此时就很容易在页面里找到该控件并调用的它的Click方法了。当页面拖放了一个LinkButton链接按钮的时候就需要使用__doPostBack函数了,因为a是不能直接Postback的,我们可以把__doPostBack函数贴过来温顾一下:

<div>
<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="5o6sLhradLF1t/FSiu1eQ4vknA9uiZViTS36nXHjkdmNX6cRl4LIwK7Kna/xh9t9WhGFmwmdJA8yojvdQLLWOjS9ohcQem358kibQCPhQH9u2ffiui2v4Ao2wCX5GlGLMmS3+pqfb8VyWktLrj1zz7z7CGnnoGScuEsfP93TSIY3HnF1+X8NZT4eJfe2FuzwRVTX/Krq1bkfc6pPs3pcWmPHokdjsr4baM3pJdV+3EM5b1FwHfaIy2bDwMl6sVRCYxvFvumOJzKktI9eColGx4KN0hYz4hz60kcvPZUeFgU=" />
</div>

<script type="text/javascript">
<!--
var theForm = document.forms['aspnetForm'];
if (!theForm) {
theForm
= document.aspnetForm;
}
function __doPostBack(eventTarget, eventArgument) {
if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
theForm.__EVENTTARGET.value
= eventTarget;
theForm.__EVENTARGUMENT.value
= eventArgument;
theForm.submit();
}
}
// -->
</script>

其实很简单,先是找到aspnetForm这个表单,然后在再__doPostBack函数判断关联的onsubmit()是否返回ture(禁止提交表单的验证),之后将eventTarget和eventArgument参数分别给form表单隐藏域赋值,之后再提交表单,__EVENTTARGET即为触发事件控件的name(即UniqueID),__EVENTARGUMENT就是事件参数了,所以通过这样PostBack到服务器端就能找到对应按纽触发的事件了。

那我如何doPostBack使之触发TinyMce自定义服务器控件的OnSave事件呢?看懂了上面的__doPostBack函数不就知道了,不就是__doPostBack('TinyMce自定义服务器控件UniqueID','eventArgument参数')吗?修改Tinymce编辑器save插件的js源代码即可,找到如下代码块:

_save : function() {
var ed = this.editor, formObj, os, i, elementId;

formObj
= tinymce.DOM.get(ed.id).form || tinymce.DOM.getParent(ed.id, 'form');

if (ed.getParam("save_enablewhendirty") && !ed.isDirty())
return;

tinyMCE.triggerSave();

// Use callback instead
if (os = ed.getParam("save_onsavecallback")) {
if (ed.execCallback('save_onsavecallback', ed)) {
ed.startContent
= tinymce.trim(ed.getContent({format : 'raw'}));
ed.nodeChanged();
}

return;
}

if (formObj) {
ed.isNotDirty
= true;

if (formObj.onsubmit == null || formObj.onsubmit() != false)
formObj.submit();

ed.nodeChanged();
}
else
ed.windowManager.alert(
"Error: No form element found.");
}

这是它原来的代码,变量ed就是当前的编辑器实例了,tinyMCE.triggerSave()是把当前编辑器内容同步保存到对应的textarea内,ed.getParam("save_onsavecallback")是获取编辑器初始化配置参数,如果设置了save_onsavecallback函数则回调函数并返回,回传ed当前编辑器对象这个参数,如果没有save_onsavecallback函数则继续下面的formObj对象判断,tinyMCE会从textarea元素一直往上找到一个form,没有找到则弹窗提示,找到了则submit表单,这就是tinyMCE编辑器按Ctrl+S键保存的处理,我现在要改的地方应该很明确了,就是不能让tingMCE就这样直接submit表单了,不然服务器端是没有办法知道是那个按钮要触发事件的,根据上面__doPostBack函数原理,我做了如下修改:

_save : function() {
var ed = this.editor, formObj, os, i, elementId;

formObj
= tinymce.DOM.get(ed.id).form || tinymce.DOM.getParent(ed.id, 'form');

if (ed.getParam("save_enablewhendirty") && !ed.isDirty())
return;

tinyMCE.triggerSave();

// Use callback instead
if (os = ed.getParam("save_onsavecallback")) {
if (ed.execCallback('save_onsavecallback', ed)) {
ed.startContent
= tinymce.trim(ed.getContent({format : 'raw'}));
ed.nodeChanged();
}

return;
}

if (formObj) {
ed.isNotDirty
= true;

if (formObj.onsubmit == null || formObj.onsubmit() != false)
{
//Get Server Postback Event Target Argruments
var eventTarget = ed.getParam('onserversave');
if (eventTarget)
{
var eventTargetElem = document.getElementById('__EVENTTARGET');
if (!eventTargetElem)
{
eventTargetElem
= document.createElement('input');
eventTargetElem.id
= '__EVENTTARGET';
eventTargetElem.name
= '__EVENTTARGET';
eventTargetElem.type
= 'hidden';
formObj.appendChild(eventTargetElem);
}
eventTargetElem.value
= eventTarget;
formObj.submit();
}
}

ed.nodeChanged();
}
else {
ed.windowManager.alert(
"Error: No form element found.");
}
}

首先是获取初始化控件UniqueID的onserversave参数,这是从服务器端动态输出的,如果TinyMce自定义服务器控件关联了OnSave事件则输出,没有这里获取的就是undefined,我这里是做了判断有才submit回发到服务器端去,然后再去找一个ID为__EVENTTARGET的hidden元素,如果页面没有生成这个元素则创建一个并添加到表单域里面,之后设置它的value为onserversave参数,这一步很重要,在服务器端__EVENTTARGET隐藏的值就是认为是触发服务器控件事件的UniqueID,本来还有一个eventArgument事件参数,但是我这里的TinyMce自定义服务器控件就只有一个OnSave事件不需要传递eventArgument事件参数来判断是触发了那个事件,所有我就没有把__EVENTARGUMENT这个值加到表单域里面去了。这样,当我们在编辑器内按下Ctrl+S键后就能Postback并触发TinyMce自定义服务器控件OnSave事件,当然这样对用户体验不够友好,因为只样毕竟是sumbit刷新页面了,噢等等!上面不是说过Tinymce编辑器有一个save_onsavecallback函数,是的,设置这个响应的js函数之后就不会sumbit到服务器端去了,这里只是说为TinyMce自定义服务器控件提供一个这样的事件。我的博客编辑器就是使用了在客户端无刷新的保存的Ajax函数,没当按下Ctrl+S键后会在编辑器的左下方提示正在保存,这样不但能及时保存文章内容不会丢失,而且页面都不会刷新给人体验也很好。^_^

好吧,熬了两个晚上,终于总结完了这几个星期使用TinyMCE在线编辑器使用的一些心得,这里非常感谢我的主管Earth,是他在广佛都市网项目里面用了这个编辑器,让我有机会了解使用TinyMCE这个免费开源的编辑器,谢谢!

本文为Jonllen原创文章,遵循知识署名方式共享,首发于个人博客原文地址http://jonllen.com/Article.aspx?aid=66,转载请保留此段声明。本文以"现状"提供,且没有任何担保,同时也没有授予任何权利。
posted on 2009-09-12 16:42  Jonllen Peng  阅读(1335)  评论(2编辑  收藏  举报