65.9K
CodeProject 正在变化。 阅读更多。
Home

适用于 Opera 的基于 Web 的富内容编辑器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.40/5 (7投票s)

2005年10月3日

CPOL

9分钟阅读

viewsIcon

84529

downloadIcon

730

编辑器使用 DOM 来改变节点属性。这使得在任何支持 DOM 的浏览器中编辑富内容成为可能。

Sample Image - Editor/Editor.gif

引言

市面上有很多基于网络的富文本编辑器。举几个例子:

在开始之前,我想声明一下,我不想被误解。所有上述富内容编辑器都做得非常出色:它们将枯燥的文本区域转换为完全可编辑的项目。它们的开发团队付出了巨大的努力来改进/增强/支持它们。我完全尊重他们的工作。本文绝不是一种补充方法。我只是想从不同的角度看待问题。仅此而已。

还有许多其他的富内容编辑器,它们提供不同的功能。所有这些编辑器都利用浏览器的 ContentEditable 模式来创建所谓的“所见即所得”效果。

然而,启用富文本内容编辑并不是一个标准。因此,每个供应商都有自己的实现方式。

此外,对 ContentEditable 模式的支持是有限的:目前只有基于 Mozilla 的浏览器和 Internet Explorer 支持它。所以,假设当第三个浏览器出现并说“嘿,各位!我也有自己的 ContentEditable 模式实现方式!”,没有人能强制他们以某种方式实现它。因为没有针对富内容创建的任何声明标准。

那会发生什么?我们将付出三倍的努力来分叉我们的代码以支持这两个浏览器。假设突然出现十个浏览器,它们有十种不同的 API 实现。那么你将面临两种选择:要么花费十倍的精力,创建一堆乱七八糟的代码来处理所有可能的情况,要么忽略一些(或大多数)少数派浏览器。

但这不应该是这样

实际上,有一种编辑 HTML 文档的标准方法,由 万维网联盟 推荐

那就是文档对象模型!

如果我们通过操作 DOM 来实现富内容的可编辑性,我们就可以将富内容支持添加到任何支持 DOM 的浏览器中;而不仅仅是 Mozilla 和 IE。

我知道我们无法支持地球上的每一个浏览器。但是,恕我直言,至少应该给所有完全支持 DOM 的浏览器平等的机会。

但是,这真的有必要吗?

这种产品的必要性是另一个问题。如上所述,绝大多数的网页使用 IE 和 Mozilla 系列浏览器,它们在内容可编辑性方面做得非常出色。

另一个讨论点可能是 WYSIWYG 在网页上的适用性和必要性。或者换句话说,使用带有 BBCode 的文本区域,并将模板管理和 WYSIWYG 内容留给 Macromedia Contribute 等应用程序包是否会更好。

根据我的网络统计,Opera 用户占我流量的不到 1%。(8-10% Mozilla,这很好 :);大约 90% IE,其余 1-2% )。

因此,人们会对在基于 Mozilla 的浏览器和 IE(占我传入网络流量的 96-99%)中运行的 WYSIWYG 编辑器感到满意。

我会满意吗?当然不会。但这只是我,也只有我。:)

无论如何,Code Project 的一个目标是展示那些看似疯狂的想法,不是吗?

也许一个想法会将其从技术思维的角度拉到更合理的层面,让普通网络用户也能利用。

我听到你们说“哲学够了,让我们看看成果”。所以我们开始吧...

实现

测试文章编辑器的最佳方式是用它写一篇文章;这样我就可以了解我可以添加哪些功能,如何简化编辑器的使用,修复错误等。所以我正在用 编辑器 编写这篇文章(抱歉,我找不到一个更有想象力的名字。:) )

信不信由你,我正在用我的第二大爱浏览器 Opera 编写这篇文章。我的第一大爱是 Mozilla;我从来没有考虑过 Internet Explorer,我真羞愧。:)

编辑器 使用了几个 js 类来运行

  • DraggableLayer:该类用于添加拖放支持和 DOM 集成。
  • ToolTip:该类用于在鼠标悬停在节点锚点(带有白色加号的图像)上时弹出微小的工具提示。
  • DOMManager:该对象用于查找父子关系,以及删除在 Opera 和 Mozilla 中引起问题的空文本节点。
  • WindowObject:该类用于获取浏览器窗口的内部尺寸和滚动偏移量。
  • TextFormatter:该对象用于删除仅用于编辑目的的额外标记。此外,它还将标签转换为小写,因此标记将或多或少地符合标准。
  • Editor:编辑器是文章编辑应用程序 GUI(视图)的模型。

使用 Editor,您可以对文章进行大量格式化,添加图像,添加链接,并且仍然可以创建有效的 XHTML-strict 代码。我尝试使 GUI 简单,但我不是可用性专家。我乐于接受任何可能进一步增强可用性的想法/草图/模型等。

编辑器对象

对于那些不熟悉 MVC (Model View Controller) 范式的人:Editor 对象是编辑器的 Model。通过将 Model 与视图分离,我们可以随心所欲地更改视图,更改布局,更改样式等,而无需更改背后的 js 代码。

以下是 Editor 的一个高度截断的原型。

_this=_Editor.prototype;

function _Editor() {
	/*initialize members*/
	/**/
}
/*public methods*/
_this.setActiveNodeName=function(strValue){...};
_this.setActiveNode=function(obj){...};
_this.getActiveNode=function(){...};
_this.setCurrentAction=function(intValue){...};		
_this.isBlockLevel=function(strName){...}
_this.isInline=function(strName){...}
_this.isActiveNodeBlockLevel=function(){...}
_this.getActiveNodeName=function(){...};
_this.getCurrentAction=function(){...};
_this.init=function(){
	this.controlPane={
		/* 
		  * Bind GUI elements to the Editor model
		  * using this associative array.
		  */
	};

	/*
	  * create span elements that contain
	  * icons which will initiate the editor when
	  * clicking on them.
	  */
	
	 /*register events*/
};

_this.getExtendedNodeDescription=function(strNode){...};
_this._RadH1_click=function(evt){...};
_this._RadH2_click=function(evt){...};
_this._RadH3_click=function(evt){...};
_this._RadH4_click=function(evt){...};
_this._RadH5_click=function(evt){...};
_this._RadH6_click=function(evt){...};
_this._RadP_click=function(evt){...};
_this._RadPre_click=function(evt){...};
_this._RadStrong_click=function(evt){...};
_this._RadEm_click=function(evt){...};
_this._RadNormal_click=function(evt){...};
_this._btnChangeType_click=function(evt){...};
_this.toggleCommitAction=function(){...};
_this._btnEdit_click=function(evt) {...};
_this._blnPreview_click=function(evt){...};
_this._btnAddAfter_click=function(evt) {...};
_this._btnAddBefore_click=function(evt){...};
_this._btnMoveAfter_click=function(evt){...};
_this._btnMoveBefore_click=function(evt){...};
_this._btnCopyHere_click=function(evt){...};
_this._btnCopyBefore_click=function(evt){...};
_this._btnCopyAfter_click=function(evt){...};
_this._btnDelete_click=function(evt){...}
_this._btnCancel_click=function(evt){...}
_this.cancelAction=function(){...};
_this._resetGUI=function(){...};
_this.createProperNode=function(strName,strText){...};
_this.getProperNodeValue=function(objNode){...};
_this.getProperNode=function(objNode){...};
_this._btnCommit_click=function(evt){
	switch(Editor.getCurrentAction()){
		case Constant.Editor.Action.ADD_AFTER:
			Editor.addAfter();		
			break;
		case Constant.Editor.Action.ADD_BEFORE:
			Editor.addBefore();
			break;
		case Constant.Editor.Action.MOVE_AFTER:
			Editor.moveAfter();
			break;
		case Constant.Editor.Action.MOVE_BEFORE:
			Editor.moveBefore();
			break;
		case Constant.Editor.Action.DUPLICATE:
			Editor.duplicate();
			break;
		case Constant.Editor.Action.DELETE_NODE:
			Editor.deleteNode();
			break;
		case Constant.Editor.Action.EDIT_TEXT:
			Editor.editText();
			break;		
		case Constant.Editor.Action.CHANGE_TYPE:
			Editor.changeType();
			break;
		default:
			break;
	}
	/*set GUI back to initial state*/
	Editor.cancelAction();
};

_this.addAfter=function(){...};
_this.addBefore=function(){...};
_this.moveAfter=function(){...};
_this.moveBefore=function(){...};
_this.duplicate=function(){...};
_this.deleteNode=function(){...};
_this.editText=function(){...};
_this.changeType=function(){...};
_this._appendControlsToFamily=function(nodeToInsert){...};
_this.organizeEditPane=function(){...};
_this.appendControls=function(theNode){...};
_this._edit_click=function(evt){...};

HTML

编辑器页面的 HTML 如下

<div id="EditorArea" class="""textEditor">""  
<h2>Introduction</h2>
</div>

<div id="EditorControls">
<h3 id="EditorInfo">Informational Heading</h3>
<div style="padding:10px;" id="EditorControlButtons">
<h4>transform</h4>
<div><input type="button" id="btnChangeType" value="change type" /></div>

<h4>alter</h4>
<div>
<input type="button" id="btnEdit" value="edit text" />
<input type="button" id="btnAddBefore" value="add before" />
<input type="button" id="btnAddAfter" value="add after" />
</div>
<h4>move</h4>
<div>
<input type="button" id="btnMoveBefore" value="move before / move up" />
<input type="button" id="btnMoveAfter" value="move after / move down" />
</div>
<h4>copy</h4>
<div>
<input type="button" id="btnCopyHere" value="duplicate element" />
</div>
<h4>remove</h4>
<div><input type="button" id="btnDelete" value="remove element" /></div>
</div>
<div style="padding:10px;">

<ul id="ListTagsBlockLevel">
... radio buttons ...
</ul>

<ul id="ListTagsInline">
.... radio buttons ...
</ul>

<textarea rows="10" cols="30" id="TxtContent"></textarea>
</div>

<div style="padding:10px;text-align:right;">
<input type="button" value="cancel" id="btnCancel" />
<input type="button" value="commit" id="btnCommit" />
</div>
</div>

EditorArea 层包含可编辑内容,而 EditorControls 层包含 GUI 控件。

为了初始化编辑器,我们在页面加载时调用其 init() 方法。

window.onload=function() {
	Editor.init();
	document.getElementById("BtnArticleSource"
	).onclick=BtnArticleSource_click;
};

function BtnArticleSource_click(evt){
	alert("This action may take some time and your browser may_
	 hang for a few seconds.\nPlease be patient.");

	var src=new EventObject(evt).getSource();

	document.getElementById("ArticleSource").value=
	TextFormatter.properHTML(
		Editor.controlPane.panel.EditorArea.getObject().innerHTML);
}

当点击“获取文章 HTML”按钮时,会触发 BtnArticleSource_click 方法。这会将文章的 HTML 填充到页面底部的 textbox 中。

您可以进一步研究代码。我尽量使代码和标记尽可能清晰。欢迎提出任何疑问和评论。

提示和使用

tip文章的创建和修改是基于节点的。如果您在编辑模式下工作,您会识别出节点锚点(inline edit iconblock level edit icon)。inline edit icon 用于添加/编辑/修改行内元素(粗体文本、强调文本、普通文本、链接和图像),而 block level edit icon 用于添加/编辑/修改块级元素(标题级别 1-6、段落和预格式化文本)。

tip点击节点锚点会弹出该特定节点的可用操作。弹窗是可拖动的 DHTML 层(我想我下一篇文章将是关于创建一个简单的拖放层)。

tip在文章中任何位置双击可在编辑预览模式之间切换。

tip就我的经验而言:习惯使用 Editor 需要一些时间。因为你需要将整个文档作为一个整体来看待,而不是一堆段落和文本,以便进行复制、粘贴和拖动。但随着你习惯它,你会变得更有创意。事实上,“复制元素”功能有时是一个很好的帮手。此外,向上和向下冒泡块级元素也很有趣。

tip适用于所有节点(行内和块级)的操作

  • 更改类型:更改节点的类型,例如,您可以将文本元素转换为链接或图像
  • 在此之前添加:在该节点之前添加一个元素
  • 在此之后添加:在该节点之后添加一个元素
  • 向上移动/上移:在节点层次结构中向上移动节点
  • 向下移动/下移:在节点层次结构中向下移动节点
  • 复制:创建一个相同的节点并将其放置在所选节点之后
  • 删除:完全删除所选节点。注意!此操作无法撤消
  • 剪切节点:删除节点并将节点内容复制到临时变量中
  • 复制节点:将节点内容复制到临时变量中;它不会删除原始节点
  • 粘贴到之前:将复制或剪切的节点粘贴到所选节点之前
  • 粘贴到之后:将复制或剪切的节点粘贴到所选节点之后

tip仅适用于行内节点的操作

  • 编辑:更改节点的内容(文本以及(如果可用)来源、链接、宽度等)。

tip行内节点的可用类型

  • 粗体文本:粗体文本.
  • 强调文本斜体文本
  • 普通文本:普通文本
  • 链接一个链接
  • 图片lovely image

tip块级节点的可用类型

  • 一级标题:一级标题
  • 二级标题:二级标题
  • 三级标题:三级标题
  • 四级标题:四级标题
  • 五级标题:五级标题
  • 六级标题:六级标题
  • 段落: 一个段落
  • 预格式化文本:预格式化文本(通常用于代码格式化)

可以改进 编辑器 的地方

从现在开始,我将把新版本的 编辑器 发布到 sardalya (http://www.sarmal.com/sardalya/)。感兴趣的人可以在那里关注更改。这里列出了一些我一眼就想到的要做的事情。不过我想提醒大家,我欢迎任何建议,无论是积极还是消极的批评,以便我能进一步改进 编辑器

  • 插入列表和嵌套列表的能力
  • 添加对其他标签的支持(如 <q><blockquote><hr />
  • 在弹出界面中添加一些描述性图标
  • 为预格式化文本添加制表符支持,这在缩进代码时非常有帮助(Opera 不会支持,但它会优雅地转换——也许可以为 Opera 和其他不支持的浏览器添加一个按钮来复制制表符(可能来自隐藏字段))
  • 更多鼠标交互(例如拖动一个锚点并将其放到另一个锚点会将节点移动到第二个锚点之前,按住 Shift 拖动会复制它等)
  • 允许选择和修改多个节点(例如,将两个段落设为粗体,将三个标题转换为段落等)
  • 允许创建和修改嵌套元素(例如 <strong><em>粗体和强调文本</em></strong>
  • 为某些操作分配键盘快捷键
  • 能够添加预定义的表情图片、项目符号图片、头像等

致读者

我只包含了我在本项目中使用的 s@rdalya API 的必要部分,以避免偏离主题并保持代码简洁。您可以在以下地址找到完整的 API:http://www.sarmal.com/sardalya/

然而,目前网络上的 API 不包括编辑器。编辑器将添加到下一个稳定版本中。

另请注意,这是一个开发版本,尚未针对 Web 进行优化。优化版本以及使用示例和文档将很快提供。

历史

  • 2005年10月3日 - 文章创建
  • 2005年10月18日 - 版本 1.1.0 发布(原版本为 1.0.0) - 文章相应更新
© . All rights reserved.