简述
Tim Berners-Lee 1990年创建的第一个浏览器支持所见即所得( WYSIWYG )方式编辑网页。Web 被设计为可读可写的媒介。然而后来的浏览器只能读取网页,只能通过表单控件输入简单的纯文本信息。
Internet Explorer 5 又重新开始支持 WYSIWYG 编辑:designMode 属性允许用户修改整个页面。然而此属性没有得到普遍使用,也许是因为它被淹没在一堆 Windows-专用的 IE 扩展中。
最近几年微软的竞争者—— Mozilla, Safari 和 Opera ——也实现了类似的编辑功能。WHATWG-工作组正致力于标准化网页编辑系统——HTML 5中引入了 designMode 和 contentEditable DOM 属性。网页的 WYSIWYG 编辑功能将是未来 Web 的重要组成部分。
本文介绍在浏览器中使用 HTML 5 编辑功能的基本方法,及可能碰到的问题。包括以下内容:
不同的开启编辑功能的方法
编辑命令
编辑产生的 HTML 代码
与 DOM 交互
本文是两篇系列文章的第一篇,第二篇将详细介绍如何实现 HTML 编辑器。
注意: 我只介绍最新版本的浏览器(Opera 9.5, Firefox 2+ 和 Safari 3)支持的编辑功能,之前版本的浏览器充满了 bug 。IE 浏览器的编辑功能从 IE 5.5 开始就没有重大改进。)
编辑系统概览
编辑系统允许编辑整个网页或部分网页。其包含以下几个特点:
插入光标( caret )显示目前插入点。用户可通过键盘或鼠标移动光标、选择文本、输入或者删除内容。
有些浏览器提供用户界面以改变元素大小或位置;这些可编辑元素包括图像、表格和表单控件。
浏览器内建了一些标准编辑命令—— Bold, Italic, InsertLink, Paste, Undo 等。可以通过快捷键或者通过 command API 利用脚本使用这些命令。使用 command API 可以轻松的实现 HTML 编辑工具栏。
通过 Range 和 Selection API 可以任意修改 HTML 文件;可以用此方法实现自定义编辑命令。
编辑系统允许用户修改 HTML 。系统不管你如何使用修改后的 HTML 文档。比如你想把修改后的网页复制到服务器中,可通过脚本实现。
在使用编辑系统时需要注意:
编辑命令的定义不是很清晰,不同的浏览器生成的 HTML 有较大差异。
IE 编辑功能自从2000年的 IE 5.5 以后就未有重大变化。其产生的 HTML 代码可能令人吃惊——你很可能在其中发现 标签!
启用编辑功能
有两种方法可在网页中创建可编辑部分 ——designMode 和 contentEditable 属性。
通过设置 document 对象的 designMode 属性为 true ,窗口或框架就变为可编辑的。(注意:在 IE 中此操作会使文档引用失效;应从 window 对象中重新获取)。通常会产生一个编辑窗口(designMode模式的 IFrame )。
所有包含文字的元素都可以通过设置 contentEditable为 true 变为可编辑的。( Firefox 2不支持contentEditable,但是 Firefox 3 和 IE, Opera 和 Safari 支持)。
键盘编辑
和其他编辑器类似,浏览器 HTML 编辑也可以使用鼠标和键盘。当文档获取 focus 后会显示插入光标,可通过鼠标或键盘移动插入光标。可通过键盘输入或删除字符。可通过键盘或鼠标移动、删除或替换选中文本。
一个很好的特性是所有的键盘编辑动作都被记录而且可以撤销。(如何使用 Undo 命令参看下文)
按下 Enter/Return 键后的处理方法比较复杂。很难定义此动作应该产生怎样的 HTML 代码,不同的上下文将产生不同的结果;不同的浏览器产生的 HTML 代码也存在很大差异。如果插入光标位于 (非空的) p 元素中,所有浏览器都应该闭合此 p 元素,插入新的(使用相同属性) 并把插入光标置于新的 p 元素。( Mozilla 会在插入光标后插入 (多余的) br 元素。)
例子如下 (在下面的例子中|符号代表插入光标位置):
bla bla|
在 IE 或 Safari 中按下回车后:
bla bla
|
如果插入光标在 (非空) h1 元素之后,所有浏览器都会关闭 h1,但 IE 和 Opera 会插入新的 p 元素并将光标置于 p 元素中;Safari 会插入新的 h1 元素并将光标置于新元素中; Mozilla 不创建任何元素,但会在光标后插入两个 br 元素。如:
bla bla|
在 IE 或 Opera 中按下回车后:
bla bla
|
但在 Mozilla 中变为:
bla bla
|
在 Safari 中变为:
bla bla
|
如果你直接在 body 元素中输入 (而不使用其他容器元素)并点击确定,在 Mozilla 中会插入 br 元素。IE 和 Opera 会把前面的文字转换成 p 元素并插入新的 p元素;而 Safari 将插入 div 元素。
如果在 div 元素中输入“回车”, Safari, Opera 和 IE 会关闭当前的 div 并插入新的div。Mozilla 会在当前div 中插入一个br 元素。
如果光标在嵌套的块级元素中( block level element),所有浏览器都会关闭最内层的元素并把光标置于次外层块元素中。
结论:这真复杂!令人惊奇的是 IE 居然是最好的实现,其总能保证块级元素的正确性。Mozilla 最烂,总用br代替块元素,这样就无法为文字设置样式了。
光标位置
插入光标可在字符间移动。但是无法看到光标相对于标签的位置。所有浏览器的处理方法一致。光标相对于块级元素的处理方法:光标总是位于最内层的块元素。不能把光标置于两段之间。
如下所示,| 符号显示可能的插入点位置:
|P|1|
|P|2|
|P|3|
|P|4|
光标相对于 inline 元素处理方法:如果光标在文字左边则被认为在所有元素外;如果光标在文字右边则被认为在元素内:
|A|B|C|
所以如果你在加粗文字 range 左边输入文字,新文字不是加粗的。如果你在 range 右边添加字符,则新文字也是加粗的。
删除
如果删除段落边界,结果是一致的 —— 左边的块 “获胜” 右边的内容会被加入到左边元素中:
Overskrift
|Text
按删除后结果如下:
Overskrift|Text
Safari 浏览器的实现方法比较聪明 (或者比较糟糕,这取决于你的喜好):
Overskrift|Text
编辑 Object
浏览器提供用户界面以编辑特殊的 HTML 对象。
IE 允许改变图像、表格、表单元素大小,或通过拖拽改变元素位置 (当选中元素后,出现拖拽图标)。
Mozilla 允许改变图像和表格大小,并允许通过附加控件创建新的行和列。Mozilla 也支持改变元素位置,此功能的 UI 是专利保护的并只能用于 Mozilla 浏览器,且无法定制使用。
编辑命令 (Editing command)
不同浏览器支持不同编辑命令。这些命令产生的 HTML 代码没有被标准化,因此不同浏览器产生结果不同。如,在 IE 中“加粗” 命令生成代码为:
Hello!
而 Safari 生成代码为:
hello!
产生的代码都是比较老式的代码风格,至少在 IE 浏览器中如此。很多编辑命令会产生已遭弃用的 font 标签 (如23);产生的 HTML 无法通过 XHTML 验证,有时甚至不是合法的 HTML!
Opera 的 HTML 编辑命令类似于 IE,也使用 元素。Safari 通过 和内联 CSS修改文本样式。Safari 方法的优点是生成的 HTML 代码可以通过 HTML 4.01 Strict 验证。
Mozilla 支持上面两种方法——既可以和 IE/Opera 产生显示性元素,也可以如 Safari 一样产生样式属性。
如果你重视 HTML 验证,你应该在服务器端实现检查功能,以把修改后网页变为合法 (X)HTML文件(无论如何你都应该这样做,以防止 XSS-攻击)。
键盘快捷键
许多编辑命令可以通过快捷键实现:如Ctrl/Cmd + B 设置加粗,Ctrl/Cmd + Z 是撤销命令等。但不同浏览器中有不同的快捷键。
不能重新设置快捷键,但可以通过脚本截获键盘事件,以重新定义快捷键功能。
command API
比如你想实现一个文本编辑工具栏,用户可以使用此工具栏改变文字格式。这可以通过命令 API 实现。此 API 和常见的 DOM API 不同,其实际上是允许脚本的 IOleCommandTarget 接口——此接口是微软用于同步工具栏的 COM 接口。
command API 在 Document 对象中,由 execCommand 方法和一系列以 “query” 开头的方法组成(这些方法返回 command 相关信息)。
所有的方法的第一个参数是 command ID ,即是字符串形式的 command 名称。command API 的方法如下所示。
ExecCommand
对选中元素执行 command 。有些命令设置/取消属性——如 bold 命令如果作用于已经加粗的文本,则会取消加粗。有些命令要求参数值——如 forecolor 命令需要颜色值。有些命令使用标准对话框——如 link 命令会显示对话框以允许用户输入 URL。无法自定义对话框,但是可以不用:
result = document.execCommand(command, useDialog, value)
上例详细解释:
command: 字符串;命令名称。
useDialog: 布尔值;是否显示 built-in 对话框 (并不是所有命令都需要对话框)。
value: 命令参数值。并不是所有命令都需要参数值;如果使用了 built-in 对话框,则此值将来自对话框。
result:如果命令被执行了则返回 true ;否则返回 false(如用户在对话框中选择了取消,或者命令无法执行)。
如果未选中任何内容 (只有插入光标),不同浏览器对于文本格式命令的解释不同。 如果插入光标位于单词中,IE 会将格式命令作用于整个词;而其他浏览器只作用于下一个字符。
QueryCommands
查询命令主要用于获取关于选中内容的信息。
QueryCommandEnabled
查询此命令是否能用于当前选中内容。如只有选中元素位于链接中才能使用 “删除链接” 命令。如果所选内容并非位于可编辑区域中,所有的命令都将被禁用。
QueryCommandState
查询此命令是否已经作用过选中内容。如当前选中内容为粗体,则 bold 命令的返回值为 true 。
QueryCommandValue
此函数返回值即为 execCommand 命令中使用的值,如ForeColor 返回当前选中内容的颜色值 (字符串形式) 。
不同浏览器的返回值格式不同。如 ForeColor 在IE中返回十六进制颜色码 (如 #ff0000),而在其他浏览器中返回 RGB 形式的颜色值,如 Rgb(255,0,0)。
有些返回值甚至根据浏览器 locale不同而不同,如 FormatBlock在 IE 中返回浏览器所用语言表示的段落名称。
像 bold 这样的命令没有返回值,只返回 false。(API 另外包含两个方法,queryCommandSupported 和 queryCommandIndeterminate,但这两个函数实现普遍质量不高,不堪重用。)
Range 和 Selection API
built-in 命令对处理一些简单问题比较有用,但无法随心所欲编辑网页。通过 Range 和 Selection API 可以任意的修改 HTML ,也可以实现自定义命令。
所有通过 DOM 的网页修改都会破坏撤销栈( undo-stack ),撤销栈用于实现撤销/重做命令。这虽然很不方便,但却是为了实现自定义命令必须付出的代价。
range/selection API 有两个核心类:
Range——网页中的字符之集。Range 可以跨多个元素。Range 有起点和终点。如果起点和终点相同,则称 range 为收缩的(collapsed)。
Selection ——代表当前选中内容。selection 包含一个高亮显示的 range 。如果选中的 range 是收缩的,则显示为一个插入光标(caret)。
(Range 和 selection 只能用于可编辑区域内。可在只读文档中创建 selection 。但是只读文档中的selection 不能是收缩的,因为只读文档无法显示 caret。)
这些概念在所有浏览器中都是一样的,但是具体的 API 却不完全相同——与其他浏览器不同,IE 使用自己定义的 range 和 selection API,而其他浏览器使用的是W3C DOM Range API 和未标准化的 selection API。
最大的不同时 IE 中 range 内容是字符串型式的 HTML 标记代码。而在 W3C DOM Range API 中 Range 内容通过 DOM 树访问。
Range 示例
下面是两种不同的实现方法:
在 IE (editWindow 即是 designMode模式下的框架)中:
var rng = editWindow.document.selection.createRange();
rng.pasteHTML("" + rng.htmlText + "");
在 Mozilla中
var rng = editWindow.getSelection().getRangeAt(0);
rng.surroundContents(document.createElement("code"));
Control 选择
IE 支持控件选择,但和 range 选择不同。点击如图形、表单控件或表格边框等对象时将被视为控件选择。
可以通过 Ctrl 键在 IE 中选择多个控件。其他浏览器中不存在控件选择概念;在这些浏览器中所有选择都是 text range 。