简介
Dojo 富文本编辑器 Dijit.Editor 的目的是像一个文字处理器那样工作。它提供了工具栏、HTML 输出和一个支持新命令、新按钮和其他特性的插件架构。并且,您不需要担心浏览器不兼容性,该编辑器支持多种浏览器,包括 Internet Explorer (IE) 6/7/8、Firefox3、Chrome 和 Safari。
在本文中,了解 Dojo 富文本编辑器的基本知识以及如何使用 Dojo 提供的插件增强它的性能。您还可以创建自己的插件,并学习如何扩展编辑器的工具栏。
Dojo 富文本编辑器
图 1 显示了典型的 Dojo 富文本编辑器。它有一个工具栏,其中包含一些有用的命令,如剪切、复制和粘贴。您可以使用 WYSIWYG 格式编辑内容。
图 1. Dojo 富文本编辑器
 这个编辑器使用起来非常简单。第一步是进入在其中使用编辑器的页面。在发出所有 dojo.require 命令的位置执行此步骤 — 通常是一个头脚本标记。还需要导入一个 CSS 文件以选择一个主题,例如 Claro。注意,正文元素的 CSS 类别被设置为 claro,如清单 1 所示。
清单 1. 导入编辑器类定义
				
<html>
<head>
	<title>Dojo Rich Text Editor</title>
	<style type="text/css">
		@import "../../../dijit/themes/claro/claro.css";
	</style>
	<script type="text/javascript">
		dojo.require("dijit.Editor");
	</script>
</head>
<body class="claro">
</body>
</html>  
   
  
 
有两种方法可以创建一个编辑器:编程方式或声明方式。这两种方法的效果是相同的。清单 2 显示了如何以声明方式创建编辑器。
清单 2. 以声明方式创建编辑器
				
<div dojoType="dijit.Editor" id="editor1">
    <p>
        This is the content of the editor.
    </p>
</div>  
   
  
 
 如果您查看创建的编辑器的 DOM 树,将发现编辑器由两部分组成:一个工具栏和一个浮动框架(iframe)区域。浮动框架的文档的designMode 属性被设置为 on,这使您可以直接编辑浮动框架的内容。这就是该编辑器的吸引人之处。
 在启用设计模式后,浏览器使您可以通过调用内置命令修改内容。例如,通过在 Firefox 中调用命令 execCommand('bold',false, null),可以将选中的一些文本设置为粗体。每个浏览器的内置命令是不一样的。幸运的是,编辑器实现了标准化,因此可以调用编辑器提供的统一 API 修改内容。
要使用 Dojo 富文本编辑器,单击工具栏中的按钮图标以触发一个事件(例如,单击),如图 2 所示。该事件将被编辑器或其 插件 捕获。通过调用内置命令或直接修改 DOM 节点,浮动框架文档的 DOM 树将被修改。
图 2. 处理编辑请求的工作流程
插件
Dojo 富文本编辑器的可插入式架构使您能够通过实现和注册插件来扩展编辑器的功能。插件可以向编辑器添加某种功能,或改变编辑器的行为。Dojo 包括若干编辑器插件,您也可以编写自己的插件。
一些插件,例如 Undo、Redo、Cut 和 Copy,在默认情况下是启用的。大多数插件有一个关联的工具栏按钮,如 FindReplace 插件。一些插件,例如 EnterKeyHandling,不需要修改工具栏就可以影响编辑器的行为。编辑器将按照如下所示加载并初始化一个插件。
-  导入必要的 CSS 文件。 某些插件可能有一些相关的 CSS 文件。CSS 文件定义了插件的 UI 布局,因此您需要首先将 CSS 文件导入到文档。 
-  在 extraPlugins列表中声明文件。插件应当在 extraPlugins列表中声明,这样编辑器才可以从声明中创建插件实例,如清单 3 所示。
 
 清单 3. 声明插件
 <div dojoType="dijit.Editor" id="editor1" extraPlugins="['FindReplace']"> <p> This is the content of the editor. </p> </div>
 清单 3 中的示例声明了一个名为 FindReplace的插件。
-  将声明的插件放入到完整的插件列表。 编辑器将把声明的插件和默认的插件一起放入到一个完整的插件列表。在这个阶段,编辑器包含一个插件名列表。实际的插件将根据如下步骤创建。 
-  创建编辑器的工具栏。 编辑器将创建 dijit.Toolbar的实例并将它附加到其 DOM 节点的子节点上,如清单 4 所示。
 
 清单 4. 创建编辑器的工具栏
 if(!this.toolbar){ // if you haven't been assigned a toolbar, create one this.toolbar = new dijit.Toolbar({ dir: this.dir, lang: this.lang }); this.header.appendChild(this.toolbar.domNode); }
 
-  根据列表创建插件实例。 这一步是关键所在。编辑器将枚举插件名列表并逐一对这些名称调用 addPlugin。在addPlugin方法中,插件将被创建,并且它们的setEditor和setToolbar方法将被调用以初始化它们的上下文。
 
 清单 5. 创建插件实例
 postCreate: function(){ ... // Create the plug-in one by one dojo.forEach(this.plugins, this.addPlugin, this); ... } addPlugin: function(/*String||Object*/plugin, /*Integer?*/index){ ... // Get the plug-in instance that is referenced by paremeter o.plugin. var o={"args":args,"plugin":null,"editor":this}; dojo.publish(dijit._scopeName + ".Editor.getPlugin",[o]); // Set the plug-in's context plugin.setEditor(this); if(dojo.isFunction(plugin.setToolbar)){ plugin.setToolbar(this.toolbar); } ... }
 
声明的所有插件都将被创建和初始化。
回页首
创建自己的插件
 如果 Dojo 工具箱提供的插件无法满足您的需要,那么可以自己构建一个插件。Dojo 富文本编辑器使您能够扩展编辑器的功能。您可以创建一个新的插件类(继承自 dijit._editor._Plugin)并将其添加到编辑器的插件列表。
编辑器插件的生命周期
 dijit._editor._Plugin 是编辑器插件的基类,通常为工具栏上的一个按钮和一些相关代码。它定义了插件的生命周期。
 当通过 addPlugin 函数将插件添加到编辑器时,插件的生命周期随即开始,通过该函数将发布一个 Dojo 主题,从而使插件能够创建自身。随后将分别调用插件的 setEditor 和 setToolbar 函数,用来将插件安装到编辑器。默认情况下,setEditor 函数将完成以下操作:
- 创建对编辑器的本地引用。
- 创建命令按钮。
-  将编辑器的 execCommand方法连接到按钮。
-  将插件的 updateState方法连接到编辑器。
 一切都很简单。setToolbar 的默认行为甚至更简单:它将先前创建的命令按钮添加到工具栏。
 此时,初始化过程已经完成。插件生命周期将进入 “无尽的” 事件驱动阶段,直到某些人破坏了整个编辑器,此时生命周期结束,将调用插件的 destroy 方法。
总结起来,生命周期的功能包括:
-  Editor.addPlugin- 插件的构造函数
-  plugin.setEditor
-  plugin.setToolbar
 
-  事件驱动阶段。持续调用 plugin.updateState。
-  Editor.destroy-  plugin.destroy
 
-  
要更好地了解细节,在接下来的部分将编写一个名为 FindReplace 的查找/替换插件。FindReplace 将在编辑器中查找包含给定关键字的文本,在找到后突出显示结果。它还支持使用其他新的内容替换关键字。要实现这种插件,您将设计一个特殊的工具栏,该工具栏将在单击命令按钮后显示。
继承 dijit._editor._Plugin
 第一步是通过继承 dijit._editor._Plugin 基类建立一个空的代码框架,如清单 6 所示。
清单 6. 新插件的基本代码框架
				
dojo.provide("dojox.editor.plugins.FindReplace");
dojo.require("dijit._editor._Plugin");
dojo.declare("dojox.editor.plugins.FindReplace", dijit._editor._Plugin, {
	// When you click the command button, you show a toolbar, 
	// instead of executing some editor commands.
	useDefaultCommand: false,
	
	// You can just use the original method.	
	// setEditor: function(editor){},
	
	setToolbar: function(editorToolbar){
		this.inherited(arguments);
		
		//TODO: Create your additional find/replace toolbar here, 
		// and append it after the editor toolbar.
	},
	
	updateState: function(){
		// You don't need to handle anything when editor state is updated. 
		// So just leave this empty.
	},
	
	destroy: function(){
		this.inherited(arguments);
		
		//TODO: Remember to destroy the toolbar you created.
	}
});
// Register this plug-in so it can construct itself when the editor publishes a topic.
dojo.subscribe(dijit._scopeName + ".Editor.getPlugin", null, function(o){
	if(!o.plugin && o.args.name.toLowerCase() === "findreplace"){
		o.plugin = new dojox.editor.plugins.FindReplace({});
	}
});  
   
  
 
扩展工具栏
 第二个任务是在编辑器的工具栏上生成一个按钮。setEditor 方法将自动完成这一过程;您所需做的就是使用不同的标签和不同的图标。清单 7 显示了最简单的方法。
清单 7. 在编辑器工具栏中创建一个 ToggleButton
				
...
dojo.declare("dojox.editor.plugins.FindReplace", dijit._editor._Plugin, {
	...
	// You'd like to use a toggle button to show/hide your own find/replace toolbar.
	buttonClass: dijit.form.ToggleButton,
	
	// As long as you provide a command, the CSS class of the button icon will be 
	// generated automatically. For this one, it will be "dijitEditorFindReplace".
	command: "findReplace",
	// You can also use localization here.
	getLabel: function(){
		return "Find and Replace";
	},
	...
});
...  
   
  
 
 在为 CSS 类 dijitEditorFindReplace 使用一个背景图片后,您可以看到示例按钮,如图 3 所示。
图 3. 示例插件的新按钮
下一步是创建查找/替换工具栏,并将它绑定到命令按钮。您需要一个用于搜索字符串的字段,一个用于替换字符串的字段,一个查找按钮,以及一个替换按钮。如果希望增加更多的功能,还可以添加其他一些内容,使其看上去更加专业,比如一个 Replace All 按钮或 Case Sensitive Search 和 Backwards Search 复选框。可以编写一个简单的、单独的小部件来实现这些内容,如清单 8 所示。
清单 8. 创建一个查找/替换工具栏
				
dojo.declare("dojox.editor.plugins.FindReplacePane", [dijit._Widget, dijit._Templated], {
	// With templates, you don't need to create all the stuff manually.
	templateString: dojo.cache("dojox.editor.plugins", "FindReplacePane.html"),
	
	// There are widgets in template. Tell the parser to parse them.
	widgetsInTemplate: true
});  
   
  
 
在 FindReplacePane.html 中,可以直接使用 dijit.Toolbar 和其他表格小部件。清单 9 中的例子使用一个标签和一个文本字段(或复选框)构成了一个新的部件,从而简化了代码。
清单 9. 查找/替换工具栏的内容
				
<div><div dojotype="dijit.Toolbar" dojoattachpoint="frToolbar">
  <div dojoattachpoint="findField" dojotype="dojox.editor.plugins._FindReplaceTextBox" 
    label="Find:"></div>
  <div dojoattachpoint="replaceField" dojotype="dojox.editor.plugins._FindReplaceTextBox" 
    label="Replace with:"></div>
  <div dojotype="dojox.editor.plugins._ToolbarLineBreak"></div>
  <div dojoattachpoint="findButton" dojotype="dijit.form.Button" 
    label="Find" showLable="true"></div>
  <div dojoattachpoint="replaceButton" dojotype="dijit.form.Button" 
    label="Replace" showLable="true"></div>
  <div dojoattachpoint="replaceAllButton" dojotype="dijit.form.Button" 
    label="Replace All" showLable="true"></div>
  <div dojoattachpoint="matchCase" dojotype="dojox.editor.plugins._FindReplaceCheckBox" 
    label="Match case"></div>
  <div dojoattachpoint="backwards" dojotype="dojox.editor.plugins._FindReplaceCheckBox" 
    label="Backwards"></div>
</div></div>  
   
  
 
 使用 dojoattachpoint,您可以以 FindReplacePane 属性的方式轻松访问小部件。dojox.editor.plugins._ToolbarLineBreak 对于多行工具栏非常有用。
现在,您已经创建了按钮和工具栏,下一步是将它们连接起来。您只需要将工具栏放到合适的位置并定义在单击开关按钮后应该执行哪些操作。清单 10 给出一个示例。
清单 10. 连接按钮和工具栏
				
// This initialization work should be done in setToolbar, 
// because you want to be sure that the editor toolbar is ready.
setToolbar: function(editorToolbar){
	// Super class will add the command button to the editor toolbar for you.
	this.inherited(arguments);
	
	// Create your find/replace toolbar, place it after the editor toolbar,
	// hide it, and start it up.
	var frtb = this.frToolbar = new dojox.editor.plugins.FindReplacePane();
	frtb.placeAt(toolbar.domNode, "after");
	dojo.style(frtb.domNode, "display", "none");
	frtb.startup();
	
	// Toggle it when your toggle button is clicked...
	this.connect(this.button, "onChange", "_toggleFindReplace");
	
	...
},
_toggleFindReplace: function(toShow){
	// Remember the original height.
	var height = dojo.marginBox(this.editor.domNode).h
	
	dojo.style(this.frToolbar.domNode, "display", toShow ? "block" : "none");
	
	// Resize the editor to maintain the height.
	this.editor.resize({h: height});
}  
   
  
 
图 4 显示了目前示例的外观。
图 4. 查找/替换工具栏
处理事件和实现功能
 接下来,是让 Find and Replace 按钮真正开始工作的步骤。可以在 setToolbar 方法中进行设置,如清单 11 所示。
清单 11. 处理事件
				
setToolbar: function(editorToolbar){
	...
	var tb = this._frToolbar = ...
	
	// Connect methods to the onClick events of the buttons.
	this.connect(tb.findButton, "onClick", "_find");
	this.connect(tb.replaceButton, "onClick", "_replace");
	this.connect(tb.replaceAllButton, "onClick", "_replaceAll");
	
	// Make the ENTER key work for the "Find" text field.
	this.connect(tb.findField, "onKeyDown", function(evt){
		if(evt.keyCode == dojo.keys.ENTER){
			this._find();
			dojo.stopEvent(evt);
		}
	});
	
	...
}  
   
  
 
 非常简单。下一步是实现 _find 功能。对于非 IE 浏览器,只需要使用 window.find 即可。对于 IE,事情变得有点复杂。需要编写一个适配器函数来消除浏览器差异,如清单 12 所示。
清单 12. 实现查找功能
				
// An adapter function to make all browsers look the same.
_findText: function(txt, isCaseSensitive, isBackwards){
	if(!txt){ return false; }
	var ed = this.editor, win = ed.window, doc = ed.document;
	var found = false;
	if(win.find){
		found = win.find(txt, isCaseSensitive, isBackwards, 
			false, false, false, false);
	}else if(doc.selection){
		/* IE */
		// Focus to restore position/selection, 
		// then shift to search from current position.
		ed.focus();
		var txtRg = doc.body.createTextRange();
		var curPos = doc.selection ? doc.selection.createRange() : null;
		if(curPos){
			txtRg.setEndPoint(isBackwards ? "EndToStart" : "StartToEnd", 
				curPos);
		}
		var flags = isCaseSensitive ? 4 : 0;
		if(backwards){
			flags = flags | 1;
		}
		found = txtRg.findText(txt, txtRg.text.length, flags);
		if(found){
			txtRg.select();
		}
	}
	return found;
},
_find: function(){
	var tb = this._frToolbar;
	return this._findText(tb.findField.get("value"), 
		tb.matchCase.get("value"), tb.backwards.get("value"));
}  
   
  
 
再次运行页面,查看效果,如图 5 所示。
图 5. 查找文本
现在,您在大多数现代浏览器中具备了有用的 Find 功能。
通过编程方式编辑内容
 Dojo 富文本编辑器提供了一个统一 API execCommand,可以使用它调用内置命令。有许多命令可用,比如 ndo、Redo、Cut、Copy、Paste、Bold 和 Italic。要获得内置命令的完整列表,请参考 dijit.Editor 文档(参见 参考资料)。
 对于示例插件,需要向编辑器插入一些文本来替换选中的文本。当然,有一个内置的命令可以完成这个操作 — inserthtml。您必须执行下面的操作:
-  检查选中的文本是否匹配 find字段中的文本。
-  如果是的话,调用 editor.execCommand("inserthtml", replaceText);
清单 13 显示了代码。
清单 13. 实现替换功能
				
        _replace: function(){
	var tb = this._frToolbar;
	var txt = tb.findField.get("value");
	if(!txt){ return false; }
	
	var repTxt = tb.replaceField.get("value") || "";
	var isCaseSensitive = tb.caseSensitive.get("value");
	var isBackwards = tb.backwards.get("value");
	var ed = this.editor;
	ed.focus();
	
	//Replace the current selected text if it matches the find field.
	var selected = dojo.withGlobal(ed.window, "getSelectedText", 
		dijit._editor.selection, [null]);
	if(selected && (isCaseSensitive ? 
		(selected === txt) : (selected.toLowerCase() === txt.toLowerCase()))){
		ed.execCommand("inserthtml", repTxt);
		return true;
	}
	return false;
}  
   
  
 
 您应该在编辑器中使用浮动框架窗口调用 getSelectedText,而不是在整个页面的窗口。图 6 显示了结果。
图 6. 替换文本
您现在了解了编写编辑器插件的基本细节。也许您可以自己实现一个 Replace All 功能作为练习。
回页首
结束语
在本文中,您了解了 Dojo 富文本编辑器的可插入式架构。这种插件机制允许您扩展编辑器的功能。通过使用 Dojo 富文本编辑器,您可以在应用程序中部署一个功能强大的文本编辑器。您可以轻松地使用和自定义编辑器,使它能够满足具体的需求。
