CKEditor 的动态数据插件






4.50/5 (5投票s)
使用 JavaScript 和一点 ASP.NET,就可以为 CKEditor 创建一个插件,允许用户从数据库中选择项目。

引言
本文演示了如何使用 ASP.NET 将服务器端数据移至客户端 JavaScript 应用程序,以及如何使用这些数据为基于 JavaScript 的 CKEditor(FCKEditor 的下一代产品)创建下拉插件。
我不会介绍 CKEditor 的安装,也不会介绍编写插件的细节。我假设读者已经安装了编辑器,并且熟悉插件开发的基础知识。如果您正在寻找这些信息,我建议访问 CKEditor 网站[^]。George Wu 的 这篇关于如何编写插件的教程[^] 也非常有帮助。
背景
我目前正在进行一个更新公司网站的项目。该项目的一部分是更新我们的内部发布系统,该系统允许管理人员撰写新闻文章和其他项目以分发给我们的现场代表。原始内容发布器使用 FCKEditor 和由中国承包商编写的几个编译模块。换句话说,我几乎没有源代码,而且大部分文档都是中文的。是的,他们承认这样做是为了让他们能够继续作为承包商。但这并没有奏效。
我在我们的测试网站上安装了 CKEditor,并轻松地使其正常工作。下一个挑战是复制几个插件,这些插件用于插入指向员工页面、公司政策手册部分和我们的表单库的链接。插件本身并不是一个巨大的挑战,但弄清楚如何将数据从我们的数据库移动到基于 JavaScript 的编辑器中却是。本文将演示我如何实现这一点。
传输数据
我最初尝试使数据可供编辑器使用的方法是将其作为 JavaScript 常量嵌入到页面本身中。这被证明是笨拙的:仅员工信息的数据就占用了页面其他所有内容更多的字节。我曾找到使用 PHP 传输数据的示例,但我们唯一可用的服务器端语言是 .NET。
事实证明,有一个对象,名为 XMLHttpRequest
,现在是 JavaScript 实现的标准部分。较旧的浏览器,如 IE5 和 IE6,可以使用一个非常相似的对象 Microsoft.XMLHTTP
。使用它们,您可以生成一个独立于页面的 Web 请求,并获取一个 JavaScript XML DOM 对象。
我使用的代码来自 W3 Schools[^],看起来是这样的:
if (window.XMLHttpRequest)
{// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp=new XMLHttpRequest();
}
else
{// code for IE6, IE5
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.open("GET","books.xml",false);
xmlhttp.send();
xmlDoc=xmlhttp.responseXML;
一旦执行完毕,xmlDoc
对象就包含一个 XML 对象,您可以从中检索数据。稍后我们将更多地看到这一点。
方法 1:静态 XML 文件
获取数据文件的明显方法是使用一个 static
XML 文件。只需将示例中的 books.xml 替换为正确的路径即可。
<Personnel>
<Person id="1" firstName="Alvin" lastName="Aardvark" />
<Person id="1" firstName="Betty" lastName="Butters" />
<Person id="1" firstName="Chris" lastName="Carson" />
<Person id="1" firstName="David" lastName="Daniels" />
</Personnel>
方法 2:别名 XML 文件
我对我的网站组织方式很挑剔:我希望将应用程序数据保存在 app_data 文件夹中,以便我能轻松找到它。缺点是 app_data 文件夹对 IIS 是不可访问的,其中的文件无法提供。与其移动(并且以我的记忆力,肯定会丢失)数据到其他地方,不如使用 ASP.NET 来“别名”该文件并使其可用。这个技巧非常简单。
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
Dim Doc As New XmlDocument
Doc.Load(Request.MapPath("/App_Data/Personnel.xml"))
Response.Buffer = True
Response.ClearContent()
Response.ClearHeaders()
Response.ContentType = "text/xml"
Response.Write(Doc.OuterXml)
Response.End()
End Sub
在 Load
事件中(在此之前无效),将您的数据加载到 XmlDocument
中,清除响应流,将页面重置为作为 XML 文件提供,将 XML 写入响应流,然后结束该页面的处理。现在,即使该文件的扩展名是 .aspx,它也会被视为 XML 数据。
方法 3:动态数据
方法 2 展示了我们如何使用 ASP.NET 来提供动态数据:而不是使用 XmlDocument.Load
,我们生成一个临时 XML 文档并提供它。
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
Dim Doc As XmlDocument = GenerateData()
Response.Buffer = True
Response.ClearContent()
Response.ClearHeaders()
Response.ContentType = "text/xml"
Response.Write(Doc.OuterXml)
Response.End()
End Sub
Protected Function GenerateData() As XmlDocument
Dim Doc As New XmlDocument
Dim Root As XmlElement = Doc.CreateElement("Personnel")
Doc.AppendChild(Doc.CreateXmlDeclaration("1.0", Nothing, Nothing))
Doc.AppendChild(Root)
Dim Sql As New StringBuilder
Dim Conn As SqlConnection = Nothing
Dim Cmd As SqlCommand = Nothing
Dim Adapter As SqlDataAdapter = Nothing
Dim DS As DataSet = Nothing
Dim Person As XmlElement = Nothing
Dim Node As XmlElement = Nothing
Sql.Append("SELECT EmpId, FirstName, LastName ")
Sql.Append("FROM Employees WHERE TerminatedDate IS NULL ")
Sql.Append("ORDER BY LastName, FirstName")
Try
Conn = New SqlConnection(ConnectionString)
Cmd = New SqlCommand(Sql.ToString, Conn)
Cmd.CommandTimeout = 300
Adapter = New SqlDataAdapter(Cmd)
DS = New DataSet
Adapter.Fill(DS)
For Each DR As DataRow In DS.Tables(0).Rows
Person = Doc.CreateElement("Person")
Person.SetAttribute("id", DR("EmpId").ToString)
Person.SetAttribute("firstName", DR("FirstName").ToString)
Person.SetAttribute("lastName", DR("LastName").ToString)
Root.AppendChild(Person)
Next
Catch ex As Exception
Throw ex
Finally
If DS IsNot Nothing Then DS.Dispose()
If Adapter IsNot Nothing Then Adapter.Dispose()
If Cmd IsNot Nothing Then Cmd.Dispose()
If Conn IsNot Nothing AndAlso Conn.State <> ConnectionState.Closed Then
Conn.Close()
Conn.Dispose()
End If
End Try
Return Doc
End Function
编写插件
下一步是编写 staff_links
插件本身。完整源代码在示例下载中。
在插件中,我们添加了一个 richcombo
插件,它在 CKEditor 核心中定义。然后我们定义 richcombo
的初始化方式以及列表中项目被点击时会发生什么。
init : function () {
if (editor.StaffData && editor.loadXMLDoc) {
var xml = editor.loadXMLDoc(editor.StaffData);
var nodes=xml.selectNodes( 'Personnel/Person' );
for ( var i = 0 ; i < nodes.length ; i++ ) {
var Person = nodes[i];
var id = Person.getAttribute('id');
var firstName = Person.getAttribute('firstName');
var lastName = Person.getAttribute('lastName');
var FirstLast = firstName + ' ' + lastName;
var LastFirst = lastName + ', ' + firstName;
this.add(id + delim + FirstLast, LastFirst, LastFirst);
}
}
},
onClick : function(value) {
var item = value.split(delim);
var id = item[0];
var name = item[1];
var v = '<a href="' + staffUrl + '?id=' + id + '">' + name + '</a>';
editor.fire('saveSnapshot');
editor.insertHtml(v);
}
editor
对象是对 CKEditor 当前实例的引用,并由编辑器的基础设施传递给插件。StaffData
属性和 loadXMLDoc
方法是在编辑器初始化的一部分添加的;稍后将详细介绍。
在 init
事件中,代码验证路径和方法都存在,然后使用它们将数据加载到 XML 对象中。selectNodes
接受一个 XPath
参数;在这种情况下,它将 nodes
分配给 Personnel
中所有 Person
元素的数组。然后它遍历每个 Person
并从中提取属性数据。调用 this.add
会将一个列表项添加到 richcombo
中。它的三个参数中,第一个是项的值:请注意,我传递了员工 ID 和姓氏,用预定义的定界符分隔。第二个参数是列表项在下拉列表中显示的方式,第三个是在用户将鼠标悬停在列表项上时出现的工具提示。此事件仅在插件首次激活时触发一次。这意味着数据请求最多只会发生一次;如果插件未使用,则永远不会发出数据请求。
onClick
事件在列表项被点击时触发。它接收列表项的值,该值被拆分为 ID 和姓名。这些用于创建一个链接,该链接作为 HTML 插入到编辑器的插入符号位置。调用 saveSnapshot
创建一个保存点,允许在需要时使用 Undo
插件。

实现:config.js
CKEditor 轻松修改编辑器的配置。在 CKEditor 安装的 根 目录中有一个名为 config.js 的文件,每次初始化编辑器实例时都会调用它。通常,它包含的函数是空的;我们在这里添加插件并设置菜单来显示它。
CKEDITOR.editorConfig = function( config )
{
config.extraPlugins = 'staff_links';
config.toolbar='Publisher';
config.toolbar_Publisher= [
['Source'],
['Print','Preview'],
['Cut','Copy','Paste','PasteText','PasteFromWord'],
['NumberedList','BulletedList','Blockquote'],
['JustifyLeft','JustifyCenter','JustifyRight','JustifyBlock'],
['Link','Unlink'],
['Undo','Redo'],
['Spellchecker','Scayt'],
['Find','Replace'],
'/',
['Bold','Italic','Underline','Strike'],
['Table','HorizontalRule','SpecialChar'],
['StaffLinks'],
['About']
];
};
在 config.extraPlugins
中,放置一个逗号分隔的所有插件列表。 我们 还定义了一个自定义菜单布局,并将该布局分配给编辑器。这有很多选项;有关详细信息,请阅读文档。请注意,StaffLinks
已添加到菜单布局中,这就是插件可见的方式。

实现:ckeditor_initialize.js
我没有将所有初始化代码放在使用编辑器的每个页面中,而是将其放在一个单独的脚本文件中。
function Init_CKEditor(ClientId) {
// Perform the replace on the given client id
var Editor = CKEDITOR.replace(ClientId);
// Add a function used by our custom plugins
Editor.loadXMLDoc = function(path) {
var xhttp=new XMLHttpRequest();
xhttp.open("GET", path, false);
xhttp.send();
return xhttp.responseXML;
}
// The data used by the plugins
Editor.StaffData = '/DynamicXml.aspx';
}
此方法接受将被转换为编辑器的对象的客户端 ID。严格来说,任何控件都可以被替换,但 CKEditor 的设计者建议坚持使用 textarea
或 div
标签。ID 被传递给 CKEDITOR.replace
,它会进行魔法处理并返回对新创建的编辑器实例的引用。
JavaScript 具有一项非常有用(也危险)的功能,可以动态地向对象添加方法和属性。接下来的两个语句利用了这一点,通过向编辑器实例添加 loadXMLDoc
方法和 StaffData
属性。这使得插件更容易找到其数据,并使 XML 加载方法对于我可能想编写的任何其他插件都全局可用。
实现:页面本身
最后一步是将所有内容整合起来,并将编辑器提供给用户。
<%@ Page Language="VB" ValidateRequest="false" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title>Test Page</title>
<script type="text/javascript" src="/ckeditor/ckeditor_source.js"></script>
<script type="text/javascript" src="/scripts/ckeditor_initialize.js"></script>
</head>
<body onload="Init_CKEditor('<%=TestText.ClientId %>');">
<form id="form1" runat="server">
<h3>Test CKEditor</h3>
<asp:TextBox ID="TestText" TextMode="MultiLine" runat="server" />
<hr style="margin:1em 0;" />
<asp:Button ID="PostbackOnlyButton" runat="server" Text="Generate Postback" />
</form>
</body>
</html>
有几点需要注意。首先,需要关闭页面验证。这是因为 staff_link
插件会注入 HTML;当数据发布时,页面验证会将此视为恶意行为。其次,脚本文件 ckeditor_source.js 必须 包含在使用编辑器的每个页面上。我们自己的 ckeditor_initialize.js 文件已包含,并且 body 的 onload
事件调用了我们的初始化脚本。这里还有一件非常有趣的事情:编辑器的宿主是一个 asp:TextBox
控件。当页面到达客户端时,ASP.NET 会将该控件转换为 textarea,因此将其传递给 CKEditor 是完全合法的。这意味着用户通过编辑器添加的文本会作为控件的 Text
属性发布回来,这打开了一些有趣的编码可能性。缺点是 CKEditor 通常会通过 Request.Form
返回数据。我需要验证这一点,但我怀疑使用 ASP 控件会导致文本被发送两次,从而降低效率。除非您有迫切的需求,否则使用 HTML 作为宿主控件可能比使用 ASP.NET 更好。
一些注意事项
有一个格式问题我无法解决:当用户点击列表项时,组合框的文本区域会更改以反映点击的项。对于这样的插件,我宁愿不更改标签。我可以深入到 richcombo
插件的源代码并修改行为,但这将改变编辑器中所有的组合框,并且在安装更新版本的 CKEditor 时几乎肯定会被覆盖。所以目前,我只称之为一项功能:跟踪最后链接的人!
另一件需要记住的事情是,CKEditor 的开发人员尚未发布关于如何为新编辑器编写插件的官方文档。换句话说,稍后版本的实现方式可能不同,或者功能与我这里使用的不同。
结论
我预计此代码中存在错误和疏漏;如果您发现任何错误,请告诉我。
文章历史
- 版本 3 - 2010 年 12 月 16 日 - 初始发布