浏览器中的HTML富文本编辑(一)

2009-09-26 10:10 | Army

http://www.army8735.org/2009/09/25/126.html

翻译的一篇文章,服务广大人民群众。这里贴不出来格式。

---

原文标题:《Rich HTML editing in the browser: part 1》

原文地址:http://dev.opera.com/articles/view/rich-html-editing-in-the-browser-part-1/
介绍

时光倒流。在世界上最早的浏览器——提姆·伯纳·李(Tim Berners-Lee)发布于1990年——诞生的时候,我们可以直接在所见即所得模式下编辑网页的内容,那时的网页被构思为一种可读可写的媒介。后来经过一段时间的发展,浏览器基本上变得只读了,除了只能在form控件中输入一些纯文本而已。

Internate Explorer 5的发布将浏览器的所见即所得编辑特性带回了主流:新的设计模式(designMode)属性允许用户编辑整个文档(document)。一开始这一特性显得多少有些疏忽,因为它最初是Windows操作系统下IE专有的。

近些年来,另一些可以和IE抗衡的浏览器——Mozilla、Safari和Opera——跟随并实现了这一IE独有的特性。排版引擎比较组织(WHATWG-group)也致力于编辑系统标准的建立——HTML5中设计模式和可编辑内容文档对象模型属性(contentEditable DOM properties)的介绍。看起来在浏览器中,所见即所得编辑最终将成为网页整体的一部分。

这篇文章利用HTML5的可编辑特性来评审现今浏览器的基本概念和挑战。这些科目包括:

* 不同的可编辑模式
* 编辑命令
* 编辑产生的HTML代码
* 和DOM的配合

这篇文章是两篇系列文章的第一篇,第二篇内容将覆盖到一个详细的例子来实现一个编辑器。

注意:我只考虑到最近的主流浏览器所支持的特性:Opera 9.5、Firefox 2+和Safari 3,较旧的版本有太多的bug和不稳定的地方了。IE中的实现直到5.5版本才有了明显的变化。
可编辑系统预览

可编辑系统允许用户编辑页面或者页面中的一部分,它有如下几个方面:

* 光标(caret)标识当前的插入点。用户可以输入、删除等。使用键盘或鼠标可以移动光标或者选区。
* 一些浏览器提供UI组件用户缩放重定位图片、表格和其它可重定位的元素(elements)。
* 内置了一系列独立的编辑命令:粗体、斜体、插入链接、粘贴、撤消等。这些可以被快捷键、脚本命令接口(script command API)调用。使用API可以轻易实现一个编辑器的工具栏(toolbar)。
* 使用范围和选区接口(range and selection API),你可以写出自己的脚本来修改任意html代码。这一特性通常用来实现自定义编辑命令。
* 可编辑系统允许你改变html代码。一旦你创建了它,它并不会真正地修改你网页的内容。举例来说,除非你写了保存修改的脚本命令,否则是无法将修改的内容存回服务器的。

这里还有两条关于可编辑系统的警告:

* 命令和编辑行为的产生是不可预知的,而产生的html代码结果在不同的浏览器中可能大相径庭。
* 直到2000年5.5版本的出现,IE浏览器中的实现才有了大幅度的变化。系统产生的html代码可能会让一些敏感的人颤栗——如果你看到最后的字体节点(<font>tag),你肯定会大吃一惊!

开启可编辑特性

这里有两种方法在网页上来创建一块可编辑的选区——设计模式(designMode)和可编辑内容(contentEditable)属性。

一个窗口(window)或者框架(frame)是以设置文档(document)对象上的设计模式属性为真(true)来开启的。(警告:在IE 中,这个属性在文档上是不存在的,必须从窗口对象上获取。)典型的可编辑框是将一个内嵌框架(iframe)的设置为设计模式。

一个包含文本的元素想要可编辑的话必须设置设计模式属性为真。(Firefox 2并不支持设计模式属性,但是在Firefox 3中却得到了支持。当然IE、Safari、Opera都早已支持。)
可编辑按键

在一个简单编辑器中,你或多或少会期望能够使用键盘和鼠标来进行编辑内容。当文档获得焦点(focus)时,光标就会显示,它可以上下左右移动。键入或者删除字符也会改变光标的位置。文本选区可以被移动、删除以及覆盖。

一个人性化的设定是所有按键编辑行为都可以自动被记录并且撤消。(后面会提到如何使用撤消工功能。)

复杂的问题出现了:当我们按下了回车键该怎么办?这样做的话并不会立刻显示所产生html代码,而且各个浏览器的结果还会根据上下文有所不同。如果光标在一个非空的p元素内,所有浏览器都会关闭这个元素,并且产生一个拥有相同属性的新元素,最后把光标定位到它里面。(Mozilla会在光标后附带插入一个多余的br换行元素。)例如(这些例子中都用竖线来表示光标所在位置):

html 代码
关于

1. <p>bla bla|</p>

<p>bla bla|</p>

按下回车键后IE和Safari会变为:

html 代码
关于

1. <p>bla bla</p>
2. <p>|</p>

<p>bla bla</p>
<p>|</p>

假如光标在一个非空的h1元素中,所有的浏览器都会关闭它。但是IE和Opera却会插入一个新的p元素,并且将光标位置置入其中。Safari会插入一个新的h1
元素并将光标位置设定在里面。Mozilla不会产生任何元素,但却会插入两个换行元素在光标后面。例如:

html 代码
关于

1. <h1>bla bla|</h1>

<h1>bla bla|</h1>

按下回车键后Opera会变为:

html 代码
关于

1. <h1>bla bla|</h1>
2. <p>|</p>

<h1>bla bla|</h1>
<p>|</p>

但在Mozilla中却会是:

html 代码
关于

1. <h1>bla bla|</h1>
2. |<br><br>

<h1>bla bla|</h1>
|<br><br>

在Safari中则是:

html 代码
关于

1. <h1>bla bla|</h1>
2. <h1>|</h1>

<h1>bla bla|</h1>
<h1>|</h1>

如果你直接在body元素中输入文本(并不包含其它元素),再按下回车键的话,Mozilla会插入一个br元素,IE和Opera会转换前面的文本放入一个p元素中并且插入一个新的p元素,Safari会插入一个新的div元素。
当在一个div元素中键入时,Safari、Opera和IE将关闭当前div元素并且插入一个新的div元素,Mozilla将插入一个br元素并且仍然呆在这个div元素中。
如果在光标外有嵌套的块级(block)元素,所有浏览器都会关闭(并且复制)最深的元素。光标依然会呆在外部块级元素内。
最关键的:令人惊讶的是,在块级元素的处理上,IE反而是最标准的!Mozilla会在部分情况下使用br元素代替块级元素,从而使得原文的显示保持正常。
光标位置

光标是在字符之间移动的。它在标签之间的定位是不可见的。逻辑上看起来所有浏览器是一致的。有关块级元素:光标总会被定位到最深的块级元素上,没法让光标位于两端之间。
例如,看下面,竖线代表所有光标可能处于的位置:

html 代码
关于

1. <p>|P|1|</p><p>|p|2|</p>
2. <div><p>|P|3|</p><div><p>|P|4|</p></div></div>

<p>|P|1|</p><p>|p|2|</p>
<div><p>|P|3|</p><div><p>|P|4|</p></div></div>

关于行级(inline)元素,假如光标在文字左侧,它会在所有行级元素边界之外;假如光标在文字右侧,则会在边界之内。例如:

html 代码
关于

1. <p>|A|<strong><em>B|</em></strong>C|</p>

<p>|A|<strong><em>B|</em></strong>C|</p>

所以假如你直接在粗体文本左侧输入字符的话,新输入的字符并不会是粗体;相反在右侧输入则会是粗体。
删除

如果你直接删除一个段落的边界,结果可想而知:左边的块(block)“获胜”了,块右边的内容被包含在了左边:

html 代码
关于

1. <h1>Overskrift</h1><p>|Text</p>

<h1>Overskrift</h1><p>|Text</p>

如果按下了回退键(backspace),结果是:

html 代码
关于

1. <h1>Overskrift|Text</h1>

<h1>Overskrift|Text</h1>

然而Safari却耍了个小聪明(或者说让人讨厌,这完全取决于你的心情),让右边段落的内容保持原来的风格(style):

html 代码
关于

1. <h1>Overskrift|<span class="Apple-style-span" style="font-size:16px;font-weight:normal;">Text</span></h1>

<h1>Overskrift|<span class="Apple-style-span" style="font-size:16px;font-weight:normal;">Text</span></h1>

对象编辑

浏览器支持一些特殊的UI特性编辑。
IE允许你用鼠标拖动对象的4个角来拉伸图像、表格、表单控制元素或者完全重定位元素(当对象获得焦点并被选择后,鼠标靠近句柄就会出现)。
Mozilla也允许你拉伸表和和图像,只是附带地允许用户创建新行新列。Mozilla还附带允许你重定位绝对定位的元素。但这些特性都是浏览器完全私有的,并且不能被自定义。
编辑命令

不同浏览器支持不同编辑命令。由命令所产生的html代码在不同浏览器中并不一样,且不一定符合标准。例如,在IE中,粗体命令会产生这样代码:

html 代码
关于

1. <strong>Hello!</strong>

<strong>Hello!</strong>

Safari则会产生这样的:

html 代码
关于

1. <span class="Apple-style-span" style="font-weight:bold;">Hello!</span>

<span class="Apple-style-span" style="font-weight:bold;">Hello!</span>

所产生的代码有些过时,至少在IE中是这样。令人畏惧的标签(如<FONT color=#ff0000>123</FONT>)会被一系列命令产生,并且生成的html代码并不符合xhtml标准有时甚至不符合html标准!
Opera的html实现接近(并不相等)IE,使用font元素等等。Safari产生格式化的span元素并用内联css。Safari这一行为的优点是所产生的html代码能通过HTML 4.01 Strict校验。
Mozilla支持两种模式——既可以产生像IE/Opera的元素也可以像Safari那样使用style属性。
如果你担心html校验的话,可以在服务器端做一些过滤清理,将那些标签杂烩转换为标准的xhtml(你可能很需要这样做,以防XSS漏洞攻击)。
快捷键

一些编辑命令支持快捷键,如Ctrl/Cmd + B是粗体、Ctrl/Cmd + B是撤消等等。然而这些快捷键变量在不同浏览器中位置还不完全一样。
快捷键的对照关系图不能被修改,但是却可以在在按键事件中截取到来重写它们。
命令程序接口

你可能想要实现一个工具栏来允许用户运行一些编辑命令,这些可以用API来完成。这些API看起来并不像典型的DOM API,它实际上是实现了IOleCommandTarget接口——这是一个微软应用程序中应用的COM接口,用来同步编辑文档的工具。
命令API基于文档对象之上并且是由一个叫执行命令(execCommand)方法实现的,并且一串以“query”开始的方法会返回命令的信息。
所有的方法都以命令ID为第一个参数——代表命令名称的字符串,剩余的是要执行的方法。
执行命令(ExecCommand)

在当前选取上执行命令,一些命令会切换状态,例如你在选取一段粗体文本之后执行粗体命令,它们就会变回普通样式。其它一些命令需要传入值参,例如foreclor需要颜色代码。
一些命令提供独立的对话框,例如链接(link)命令显示一个对话框来输入url地址,这些对话框不能被自定义,但是却可以被禁止掉。例如:

js 代码
关于

1. result = document.execCommand(command, useDialog, value);

result = document.execCommand(command, useDialog, value);

不同参数的含义:

* command:String,命令的名字。
* useDialog:Boolean,显示内建对话框(并不是所有命令都有对话框)。
* value:命令所需要的值,并不是所有命令都需要值;假如一个内建的对话框被显示了,那么值是从对话框里获取的。
* 结果:如果命令成功执行,则返回true;如果被用户所取消(用户取消了对话框)或者执行失败,则返回false。

当没有选择文字的情况下(只有一个光标),所有浏览器都支持文本格式化命令。假如光标在一个单词中间,IE会将整个单词格式化掉,而其它浏览器仅仅格式化下一个将要输入的字符,除非光标提前移动了。
查询命令(QueryCommand)

相对于和文档选区(document selection)相关的工具栏按钮来说,使用查询命令来查询它们的状态,这可能是所有浏览器中最健全的支持了。
开启查询命令(QueryCommandEnabled)
根据当前选区的命令是否可以运行,查询命令处于开启或关闭的状态下。例如:“解除链接(unlink)”只有当光标在一个链接(link)选区内才可用。而当光标处于不可编辑的区域中的话,所有命令都不可用。
查询命令状态(QueryCommandState)
它标识着当前选区内是否已经运行过相关命令了。例如在一个粗体选区中,粗体命令(bold command)的状态就是true。
查询命令返回值(QueryCommandValue)
它是目标选区执行命令后返回的值,对应于执行命令中传入的参数值。如foreColor返回当前选区的颜色代码(String)。

不同浏览器之间的格式化命令是不同的。例如,foreColor命令在IE中会返回一个16进制的颜色代码(如#ff0000),其它浏览器则返回一个rbg表达式(如rgb(255,0,0))。
一些返回值是建立在浏览器本地基础上的。例如在IE中,格式化块(FormatBlock)会根据浏览器的UI语言返回段落的名称。
像粗体这样的命令,返回值总是false。(api包含两个附加的方法,queryCommandSupported和queryCommandIndeterminate,但是它们用起来极不可靠。)
范围和选区API

内建的命令经常被用在实际的选区上,但是却不能修改它们的行为或者自定义实现。使用范围和选区(Range and Selection)API,你可以实现自主的html转换,就像模拟自定义实现一样。

需要说明的是,一些转换破坏了经常被撤销/重做(undo/redo)命令使用的撤销栈(undo stack),这是非常不友好的。但是它却实现了自定义命令的功能,是否值得这样做取决于你的你的页面建立目标。

范围和选区API包含两个核心类:

* 范围——一个文档中连续的字符串范围。范围可能会在元素边界上重叠。一个范围拥有开始点(start point)和结束点(end point)。如果开始点的位置和结束点相等的话,我们就说这个范围是折叠的。
* 选区——描述当前用户选取的文档选区。一个选区包括唯一的一个高亮范围。如果范围是折叠的,那么选区就以光标形态显示。

(范围和选区可以用在可编辑区域之外,你可以在只读的文档上创建选区,但是这样的话它不能被折叠,并且只读选区并不显示光标。)

这些概念在所有浏览器中通用,但是可用的API却在IE和其它浏览器中有区别。IE使用它私有的范围选区API,其它浏览器则使用W3C标准的范围API和非标准的选区API。

最主要的差别还是:IE的范围是以存储包括html标记在内的String为基础的;而W3C标准则是存储DOM节点树。
范围例子:

为了展示二者不同之处,这有一个在当前选区插入行内元素code的代码。

IE中(editWindow是个对处于designMode的frame的引用):

js 代码
关于

1. var rng = editWindow.document.seletion.createRange();
2. rng.parseHTML("" + rng.htmlText + "");

var rng = editWindow.document.seletion.createRange();
rng.parseHTML("" + rng.htmlText + "");

Mozilla中:

js 代码
关于

1. var rng = editWindow.getSelection().getRangeAt(0);
2. rng.surroundContents(document.createElement("code"));

var rng = editWindow.getSelection().getRangeAt(0);
rng.surroundContents(document.createElement("code"));

控制选区(Control selection)

IE支持控制选区,它和普通的范围选区不一样。当你在一副图片上、表单上、表格的边线上点击的时候,控制选区就产生了。

并且你可以按下Ctrl键来达到一次性选择多个控制选区的目的,其它浏览器并不支持——它们仍会当作一个文本选区来看待。
总结

本文带你浏览了基于浏览器的可编辑概念。系列文章的第二篇将给你展示一个很有价值例子——如何利用这些API来实现可编辑的页面。