玩了tiny_mce在线编辑器好几个星期,今天终于差不多把所有的功能都给完成了,确切的说是把编辑器的插件功能完美的整合在我的博客里面,解决一些小的bug,这还得意于它本身是开源免费的,这里我实现的功能主要有:
- 修改图片和多媒体文件上传和浏览功能;
- 增加signature个性签名(关联博客)和insertcode插入代码(整合CodeHighlighter代码高亮显示)功能插件;
- 修改编辑器内按下Ctrl+S键save保存插件功能,使之支持Postback到服务器端并触发OnSave事件。
- 修正编辑器内字体过小、设置编辑器不会自动移除div元素节点的等问题。
起初我引用tiny_mce编辑器都是直接嵌入的脚本的,摸索了一番待完善所有功能后,当然就要把它做成.net的自定义控件了,方便每一个页面调用,下面我就结合在做自定义控件的时候说一下Tinymce编辑器。
试试我的TinyMCE在线编辑器
先上自定义控件源代码:
TinyMce服务器自定义控件代码
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.ComponentModel;
namespace Studio.Web
...{
/**//// <summary>
/// Name : TinyMce在线编辑器
/// Author : Jonllen
/// Site : www.jonllen.com
/// CreateDate : 2009-09-09 23:23
/// UpdateDate : 2009-09-10 21:18
/// </summary>
public class TinyMce : WebControl, INamingContainer, IPostBackEventHandler //IPostBackDataHandler
...{
private string mode = "exact";
[Description("模式")]
public string Mode
...{
get ...{ return mode; }
set ...{ mode = value; }
}
private string theme = "advanced";
[Description("主题")]
public string Theme
...{
get ...{ return theme; }
set ...{ theme = value; }
}
private string language = "zh";
[Description("语言")]
public string Language
...{
get ...{ return language; }
set ...{ language = value; }
}
private string plugins = "signature,save,safari,pagebreak,style,layer,table,advhr,advimage,advlink,emotions,iespell,inlinepopups,insertdatetime,preview,media,searchreplace,print,contextmenu,paste,directionality,fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras,template,insertcode,uploadImage";
[Description("插件")]
public string Plugins
...{
get ...{ return plugins; }
set ...{ plugins = value; }
}
private string theme_advanced_buttons1 = "styleprops,formatselect,fontselect,fontsizeselect,separator,forecolor,backcolor,separator,bold,italic,underline,strikethrough,charmap,separator,bullist,numlist,separator, justifyleft, justifycenter, justifyright";
[Description("第一排按钮")]
public string ThemeAdvancedButtons1
...{
get ...{ return theme_advanced_buttons1; }
set ...{ theme_advanced_buttons1 = value; }
}
private string theme_advanced_buttons2 = "undo,redo,cut,copy,paste,face,separator,outdent,indent,removeformat,cleanup,separator,link,unlink,image,uploadImage,media,separator,save,emotions,signature,insertcode,separator,visualaid,fullscreen,preview,code";
[Description("第二排按钮")]
public string ThemeAdvancedButtons2
...{
get ...{ return theme_advanced_buttons2; }
set ...{ theme_advanced_buttons2 = value; }
}
private string theme_advanced_buttons3;
[Description("第三排按钮")]
public string ThemeAdvancedButtons3
...{
get ...{ return theme_advanced_buttons3; }
set ...{ theme_advanced_buttons3 = value; }
}
private string theme_advanced_buttons4;
[Description("第四排按钮")]
public string ThemeAdvancedButtons4
...{
get ...{ return theme_advanced_buttons4; }
set ...{ theme_advanced_buttons4 = value; }
}
private string valid_elements = "script[language|type|src],style[media,type],input[id|name|type|value|style|class|checked|disabled|title|onclick|ondblclick|onchange|onkeydown|onkeypress|onkeyup|onfocus|escape],textarea[id|name|style|class|rows|cols],form[id|name|action|method|target|enctype|onsubmit],object[id|classid|width|height],param[name|value],embed[id|src|width|height|wmode|flashvars|type],dl,dt,dd,fieldset,legend,label,h1,h2,h3,h4[class|id|style],div[class|id|style],pre,br,p[class|id|style],strong[class|id|style],em,span[style|class|id],ul[style|class|id]],li,ol,hr[width|size|noshade|style],img[title|src|border|alt|width|height|style|id|class|onclick],a[href|target|style|class|id]],table[style|border|cellspacing|cellpadding|align|class|id],tr[style|align|valign],th[style|align|valign|colspan|rowspan],td[style|align|valign|colspan|rowspan]";
[Description("验证的元素")]
public string ValidElements
...{
get ...{ return valid_elements; }
set ...{ valid_elements = value; }
}
private bool convert_fonts_to_spans = true;
[Description("是否将Font标签转化为Span")]
public bool ConvertFontsToSpans
...{
get ...{ return convert_fonts_to_spans; }
set ...{ convert_fonts_to_spans = value; }
}
private bool theme_advanced_resizing = true;
[Description("是否可缩放编辑器大小")]
public bool ThemeAdvancedResizing
...{
get ...{ return theme_advanced_resizing; }
set ...{ theme_advanced_resizing = value; }
}
private string content_css = "/tiny_mce/css/content.css";
[Description("编辑器样式文件路径")]
public string ContentCss
...{
get ...{ return content_css; }
set ...{ content_css = value; }
}
private bool relative_urls = true;
[Description("是否使用相对路径(编辑器内图片、链接等内容)")]
public bool RelativeUrls
...{
get ...{ return relative_urls; }
set ...{ relative_urls = value; }
}
private bool remove_script_host = true;
[Description("是否移除脚本文件的主机域名(编辑器内)")]
public bool RemoveScriptHost
...{
get ...{ return remove_script_host; }
set ...{ remove_script_host = value; }
}
private bool convert_urls = true;
[Description("是否自动转换路径(编辑器内图片、链接等内容)")]
public bool ConvertUrls
...{
get ...{ return convert_urls; }
set ...{ convert_urls = value; }
}
private string save_onsavecallback;
[Description("按下Ctrl+S键后触发的JavaScript客户端函数")]
public string SaveOnsavecallback
...{
get ...{ return save_onsavecallback; }
set ...{ save_onsavecallback = value; }
}
private string setup;
[Description("初始化编辑器设置的JavaScript客户端函数")]
public string Setup
...{
get ...{ return setup; }
set ...{ setup = value; }
}
private string scriptsrc = "/tiny_mce/tiny_mce.js";
[Description("编辑器主脚本文件路径")]
public string ScriptSrc
...{
get ...{ return scriptsrc; }
set ...{ scriptsrc = value; }
}
[Description("文本域控件的高度(以字符为单位)")]
public int Rows
...{
get ...{ return this.textArea.Rows; }
set ...{ this.textArea.Rows = value; }
}
[Description("文本域控件的宽度(以字符为单位)")]
public int Cols
...{
get ...{ return this.textArea.Cols; }
set ...{ this.textArea.Cols = value; }
}
[Description("文本域内容")]
public string Text
...{
get ...{ return this.textArea.Value; }
set ...{ this.textArea.Value = value; }
}
[Description("开始标记和结束标记之间的文本")]
public string InnerText
...{
get
...{
string text = this.textArea.Value;
text = System.Text.RegularExpressions.Regex.Replace(text, "<[^>]+>", "");
text = System.Text.RegularExpressions.Regex.Replace(text, "&[^;]+;", "");
return text.Replace("\r\n", "").Trim();
}
}
private Unit width;
/**//// <summary>
/// 重写父类Width属性指向textarea控件的宽度
/// </summary>
[Description("文本域宽度")]
public override Unit Width
...{
get ...{ return width; }
set ...{ width = value; this.textArea.Style["width"] = value.ToString(); }
}
private Unit height;
/**//// <summary>
/// 重写父类Height属性指向textarea指向的高度
/// </summary>
[Description("文本域高度")]
public override Unit Height
...{
get ...{ return height; }
set ...{ height = value; this.textArea.Style["height"] = value.ToString(); }
}
private string cssClass;
/**//// <summary>
/// 重写父类CssClass属性指向textarea控件的class样式类
/// </summary>
[Description("文本域样式类")]
public override string CssClass
...{
get ...{ return cssClass; }
set ...{ cssClass = value; this.textArea.Attributes["class"] = value; }
}
/**//// <summary>
/// 重写父类ClientID只读属性指向textarea控件生成的客户端ID
/// </summary>
public override string ClientID
...{
get
...{
return base.ClientID + "_textarea";
}
}
/**//// <summary>
/// 服务器端保存事件
/// </summary>
public event EventHandler Save;
/**//// <summary>
/// IPostBackEventHandler成员
/// </summary>
/// <param name="eventArgument">传递的参数</param>
public void RaisePostBackEvent(string eventArgument)
...{
if (this.Save != null)
this.Save(this, new EventArgs());
}
private HtmlTextArea textArea;
private string itemKey = "TinyMce";
public TinyMce()
...{
//初始化HtmlTextArea,并添加到子控件内
this.textArea = new HtmlTextArea();
this.textArea.ID = "textarea";
//引起回发的Html元素的name属性值必须为控件的 UniqueID,否则 RaisePostBackEvent 事件不会被调用
this.textArea.Name = this.UniqueID;
this.Controls.Add(textArea);
}
重写父类Render生成HTML方法#region 重写父类Render生成HTML方法
protected override void Render(HtmlTextWriter writer)
...{
//注释父类Render的方法,因为WebControl默认会生成一个<span>标签
//base.Render(writer);
//调用HtmlTextArea控件的Render方法生成HTML
this.textArea.RenderControl(writer);
//当前上下文为空则返回(在VS的ASPX设计视图页面)
if (Context == null)
...{
return;
}
//生成Tiny_Mce在线编辑器脚本的字符串
System.Text.StringBuilder sb = new System.Text.StringBuilder();
int count = 0;
int.TryParse( Convert.ToString( HttpContext.Current.Items[this.itemKey]) ,out count);
//获取当前页面的编辑器数量,如果只有一个则引用Tiny_Mce在线编辑器主脚本文件,避免多个重复引用
if ( count == 0)
...{
sb.AppendFormat("<script type=\"text/javascript\" src=\"{0}\"></script>", this.scriptsrc);
}
HttpContext.Current.Items[this.itemKey] = count + 1;
sb.Append("<script type=\"text/javascript\">");
sb.Append("tinyMCE.init({");
sb.AppendFormat("mode : \"{0}\",", this.mode);
if (!string.IsNullOrEmpty(this.textArea.ClientID))
...{
sb.AppendFormat("elements : \"{0}\",", this.textArea.ClientID);
}
sb.AppendFormat("theme : \"{0}\",", this.theme);
if (this.theme != "simple")
...{
sb.AppendFormat("language : \"{0}\",", this.language);
sb.AppendFormat("plugins : \"{0}\",", this.plugins);
if (!string.IsNullOrEmpty(this.theme_advanced_buttons1))
...{
sb.AppendFormat("theme_advanced_buttons1 : \"{0}\",", this.theme_advanced_buttons1);
}
if (!string.IsNullOrEmpty(this.theme_advanced_buttons2))
...{
sb.AppendFormat("theme_advanced_buttons2 : \"{0}\",", this.theme_advanced_buttons2);
}
if (!string.IsNullOrEmpty(this.theme_advanced_buttons3))
...{
sb.AppendFormat("theme_advanced_buttons3 : \"{0}\",", this.theme_advanced_buttons3);
}
if (!string.IsNullOrEmpty(this.theme_advanced_buttons4))
...{
sb.AppendFormat("theme_advanced_buttons4 : \"{0}\",", this.theme_advanced_buttons4);
}
sb.AppendFormat("valid_elements : \"{0}\",", this.valid_elements);
sb.AppendFormat("convert_fonts_to_spans : {0},", this.convert_fonts_to_spans ? "true" : "false");
sb.AppendFormat("theme_advanced_resizing : {0},", this.theme_advanced_resizing ? "true" : "false");
sb.AppendFormat("relative_urls : {0},", this.relative_urls ? "true" : "false");
sb.AppendFormat("remove_script_host : {0},", this.remove_script_host ? "true" : "false");
sb.AppendFormat("convert_urls : {0},", this.convert_urls ? "true" : "false");
if (!string.IsNullOrEmpty(this.save_onsavecallback))
...{
sb.AppendFormat("save_onsavecallback : {0},", this.save_onsavecallback);
}
if (!string.IsNullOrEmpty(this.setup))
...{
sb.AppendFormat("setup : {0},", this.setup);
}
if (string.IsNullOrEmpty(this.save_onsavecallback) && this.Save != null)
...{
//输出服务器端触发回发事件的eventTarget参数到tinymce编辑器初始话参数
sb.AppendFormat("onserversave : \"{0}\",", this.UniqueID);
}
}
sb.AppendFormat("content_css : \"{0}\"", this.content_css);
sb.Append("});");
sb.Append("</script>");
//输出
writer.WriteLine(sb.ToString());
}
#endregion
}
}
首先做一个自定义控件需要考虑的继承类,是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这个免费开源的编辑器,谢谢!