表单Form

前端开发
2018年12月15日
474

表单的基础知识

在HTML中,表单是由<form>元素来表示的,而在JavaScript中,表单对应的则是HTMLFormElement类型。HTMLFormElement继承了HTMLElement,因而与其他HTML元素具有相同的默认属性。不过,HTMLFormElement也有它自己独有的属性和方法:

  • acceptCharset:服务器能够处理的字符集。
  • action:接受请求的URL。
  • elements:表单中所有控件的集合(HTMLCollection)。
  • enctype:请求的编码类型。
  • length:表单中控件的数量。
  • method:要发送的HTTP请求类型,通过是“get”或“post”。
  • name:表单的名称。
  • target:用于发送请求和接收响应的窗口名称。
  • reset():将所有表单域重置为默认值。
  • submit():提交表单。

取得<form>元素引用的方式有好几种:

js
// 看成与其他元素一样,通过ID特性获取 var form = document.getElementById('form1'); // 通过document.forms可以取得页面中所有的表单。 var firstForm = document.forms[0]; // 取得页面中的第一个表单 var myForm = document.forms['form2']; // 取得页面中名称为'form2'的表单

另外,在较早的浏览器或那些支持向后兼容的浏览器中,也会把每个设置了name特性的表单作为属性保存在document对象中。例如,可以通过document.form2可以访问到名为“form2”的表示,不推荐使用这种方式:一是容易出错,二是将来的浏览器可能会不支持。

注意,可以同时为表单指定idname属性,但它们的值不一定相同。

提交表单

用户点击提交按钮或图像按钮时,就会提交表单。

html
<!-- 通用提交按钮 --> <input type="submit" value="Submit Form"> <!-- 自定义提交按钮 --> <button type="submit">Submit Form</button> <!-- 图像按钮 --> <input type="image" src="graphic.gif">

只要表单中存在上面列出的任何一种按钮,那么在相应表单控件拥有焦点的情况下,按回车键可以提交表单(textarea是一个例外,在文本区中回车会换行)。

以这种方式提交表单时,浏览器会在将请求发送给服务器之前触发submit事件。这样,我们就有机会验证表单数据,并据以决定是否允许表单提交。阻止这个事件的默认行为就可以取消表单提交。

js
var form = document.getElementById('myForm'); // 这里使用了之前定义的EventUtil对象,以便跨浏览器处理事件。 EventUtil.addHandler(form, 'submit', function (event) { // 取得事件对象 event = EventUtil.getEvent(event); // 阻止默认事件 EventUtil.preventDefault(event); });

在JavaScript中,以编程方式调用submit()也可以提交表单。而且,这种方式无需表单包含提交按钮,任何时候都可以正常提交表单。

js
var form = document.getElementById('myForm'); form.submit();

重置表单

在用户单击重置按钮时,表单会被重置。

html
<!-- 通用重置按钮 --> <input type="rest" value="Reset Form"> <!-- 自定义重置按钮 --> <button type="reset">Reset Form</button>

用户单击重击按钮时,会触发reset事件。利用这个事件,我们可以在必要时取消重置操作。

js
var form = document.getElementById('myForm'); EventUtil.addHandler(form, 'reset', function (event) { // 取得事件对象 event = EventUtil.getEvent(event); // 阻止默认事件 EventUtil.preventDefault(event); });

与提交表单一样,也可以通过JavaScript来重置表单。

js
var form = document.getElementById('myForm'); form.reset();

表单字段

可以像访问页面中的其他元素一样,使用原生的DOM方法访问表单元素。此外,每个表单都有elements属性,该属性是表单中所有表单元素(字段)的集合。这个集合是一个有序列表,每个表单字段在elements集合中的顺序与它们出现在标记中的顺序相同,可以按照位置和name特性来访问它们。

js
var form = document.getElementById('form1'); // 取得表单中的第一个字段 var field1 = form.element[0]; // 取得名为'textbox1'的字段 var field2 = form.elements['textbox1']; // 取得表单中包含字段的数量 var fieldCount = form.elements.length;

如果有多个表单控件都在使用一个name(如单行按钮),那么就会返回以该name命名的一个NodeList。

html
<form method="post" id="myForm"> <ul> <li><input type="radio" name="color" value="red"></li> <li><input type="radio" name="color" value="green"></li> <li><input type="radio" name="color" value="blue"></li> </ul> </form>
js
var form = document.getElementById('myForm'); var colorFields = form.elements['color']; alert(colorFiles.length); // 3 var firstColorField = colorFields[0]; var firstFormField = form.elements[0]; alert(firstColorField === firstFormField); // true

共有的表单字段属性

除了<fieldset>元素之外,所有表单字段都拥有相同的一组属性。由于<input>类型可以表示多种表单字段,因此有些属性只适用于某些字段,但还有一些属性是所有字段共有的。表单字段共有的属性如下:

  • disabled:布尔值,表示当前字段是否被禁用。
  • form:指向当前字段所属表单的指针:只读。
  • name:当前字段的名称。
  • readOnly:布尔值,表示当前字段是否只读。
  • tabIndex:当前字段的切换(tab)序号。
  • type:当前字段的类型,如’checkbox’、'radio’等。
  • value:当前字段将被提交给服务器的值。对于文件字段来说,这个属性是只读的。

除了form属性之外,可以通过JavaScript动态修改其他任何属性。

能够动态修改表单字段属性,意味着我们可以在任何时候,以任何方式来动态操作表单。例如:禁用提交按钮,防止重复提交。

js
// 避免多次提交表单 EventUtil.addHandler(form, 'submit', function (event) { event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); // 取得提交按钮 var btn = target.elements['submit-btn']; // 禁用提交 btn.disabled = true; })

除了<fieldset>之外,所有表单字段都有type属性。对于<input>元素来说,这个值等于HTML特性type的值。

共有的表单字段方法

每个表单字段都有两个方法:focus()blur()。其中,focus()方法用于将浏览器的焦点设置到表单字段,即激活表单字段,使其可以响应键盘事件。

js
EventUtil.addHandler(window, 'load', function (event) { document.forms[0].elements[0].focus(); })

HTML5为表单字段新增了一个autofocus属性。在支持这个属性浏览器中,只要设置了这个属性,不用JavaScript就能自动地把焦点移到相应字段。

html
<input type="text" autofocus>

blur()的作用是从元素中移走焦点。

js
document.forms[0].elements[0].blur();

共有表单字段事件

所有表单字段都支持下列3个事件:

  • blur:当前字段失去焦点时触发。
  • change:对于<input><textarea>元素,在它们失去焦点且value值改变时触发;对于<select>元素,在其选项改变时触发。
  • focus:当前字段获得焦点时触。

文本框脚本

在HTML中,有两种方式来表现文本框:一种是使用<input>元素的单行文本框,另一种是使用<textarea>的多行文本框。

要表现文本框,必须将<input>元素type特性设置为"text"。而通过设置size特性,可以指定文本框中能够显示的字符数。通过value特性可以设置文本框的初始值,而maxlength特性则用于指定文本框可以接受最大的字符数。

html
<input type="text" value"这是一个单行文本框" size="25" maxlength="50">

相对而言,<textarea>元素则始终会呈现一个多行文本框。要指定文本框的大小,可以使用rowscols特性。其中,rows指定的是文本框的字符行数,而cols指定的是文本框的字符列数。

html
<textarea rows="25" cols="5">这是一个多行文本框</textarea>

这两种文本框都会将用户输入的内容保存在value属性中。可以通过这个属性读取和设置文本框的值。

js
var textbox = document.forms[0].elements['textbox1']; alert(textbox.value); textbox.value = 'Some new value';

选择文本

上述两种文本框都支持select()方法,这个方法用于选择文本框中的所有文本。在调用select()时,大多数浏览器(除了Opera)都会将焦点设置到文本框中。

js
var textbox = document.forms[0].elements['textbox1']; textbox.select();

在文本框获得焦点时选择其所有文本,这是一种非常常见的做法,特别是在文本框包含默认值的时候。

js
EventUtil.addHandler(textbox, 'focus', function (event) { event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); target.select(); });

选择(select)事件

select()方法对应的,是一个select事件。在选择了文本框中的文本时,就会触发select事件。不过,到底什么时候触发select事件,还会因浏览器而异。在IE9+、Opera、Firefox、Chrome和Safari中,只有用户选择了文本(而且要释放鼠标),才会触发select事件。而有IE8及更早版本中,只要用户选择了一个字母(不必释放鼠标),就会触发。另外,在调用select()方法时,也会触发select事件

js
var textbox = document.forms[0].elements['textbox1']; EventUtil.addHandler(textbox, 'select', function (event) { alert(textbox.value); })

取得选择的文本

虽然通过select事件我们可以知道用户什么时候选择了文本,但仍然不知道用户选择了什么文本。HTML5通过一些扩展方案解决了这个问题。该规范采取的办法是添加了两个属性:selectionStartselectEnd。这两个属性中保存的是基于0的数值,表示所选择文本的范围(即文本选区开头和结尾的偏移量)。

js
function getSelectedText (textbox) { return textbox.value.substring(textbox.selectionStart, textbox.selectionEnd); }

IE9+、Firefox、Safari、Chrome和Opera都支持这两个属性,IE8及之前版本不支持。

IE8及之前版本提供了一个document.selection对象,其中保存着用户在整个文档范围内选择的文本信息。

js
function getSelectedText (textbox) { if (typeof textbox.selectionStart === 'number') { return textbox.value.substring(textbox.selectionStart, textbox.selectionEnd); } else if (document.selection) { return document.selection.createRange().text; } }

选择部分文本

HTML5也为选择文本框中的部分文本提供了解决方案,即最早由Firefox引入的setSelectionRange()方法。现在除select()方法之外,所有文本框都有一个setSelectionRange()方法。这个方法接收两个参数:要选择的每一个字符的索引和要选择的最后一个字符之后的字符的索引。

js
textbox.value = 'Hello world!'; // 选择所有文本 textbox.setSelectionRange(0, textbox.value.length); // 'Hello world!' // 选择前3个字符 textbox.setSelectionRange(0, 3); // 'Hel' // 选择第4到第6个字符 textbox.setSelectionRange(4, 7); // 'o w'

要看到选择的文本,必须在调用setSelectionRange()之前或之后立即将焦点设置到文本框。IE9、Firefox、Safari、Chrome和Opera支持这种方案。

IE8及更早版本支持使用范围选择部分文本。

js
function selectText (textbox, startIndex, stopIndex) { if (textbox.setSelectionRange) { textbox.setSelectionRange(startIndex, stopIndex); } else if (textbox.createTextRange) { // IE8及之前版本 var range = textbox.createTextRange(); range.collapse(true); range.moveStart('character', startIndex); range.moveEnd('character', stopIndex - startIndex); range.select(); } textbox.focus(); }

过滤输入

屏蔽字符

响应向文本框中插入字符操作的是keypress事件。因此,可以通过阻止这个事件的默认行为来屏蔽此类字符。

js
// 屏蔽非数值输入 EventUtil.addHandler(textbox, 'keypress', function (event) { event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); var charCode = EventUtil.getCharCode(event); // charCode > 9 是为了防止Firefox和Safari3.1版本之前的上下方向键、退格键和删除键触发keypress事件 // !event.ctrlKey确保用户没有按下Ctrl键 if (!/\d/.test(String.fromCharChode(charCode)) && charCode > 9 && !event.ctrlKey) { EventUtil.preventDefault(event); } });

操作剪贴板

下列是6个剪贴板事件:

  • beforecopy:在发生复制操作前触发。
  • copy:在发生复制操作时触发。
  • beforecut:在发生剪切操作前触发。
  • cut:在发生剪切操作时触发。
  • beforepaste:在发生粘贴操作前触发。
  • paste:在发生粘贴操作时触发。

由于没有针对剪贴板操作的标准,这些事件及相关对象会因浏览器而异。在Safari、Chrome和Firefox中,beforecopybeforecutbeforepaste事件只会在显示针对文本框的上下文菜单(预期将发生剪贴板事件)的情况下触发。但是,IE会在触发copycutpaste事件之前先行触发这些事件。至于copycutpaste事件,只要是在上下文菜单中选择了相应选项,或者使用了相应的键盘组合键,所有浏览器都会触发它们。

在实际的事件发生之前,通过beforecopybeforecutbeforepaste事件可以在向剪贴板发送数据,或者从剪贴板取得数据之前修改数据。不过,取消这些事件并不会取消对剪贴板的操作——只有取消copycutpaste事件,才能阻止相应操作发生。

要访问剪贴板中的数据,可以使用clipboardData对象:在IE中,这个对象是window对象的属性;而在Firefox4+、Safari和Chrome中,这个对象是相应event对象的属性。但是,在Firefox、Safari和Chrome中,只有在处理剪贴板事件期间clipboardData对象都有效,这是为了防止对剪贴板的未授权访问;在IE中,则可以随时访问clipboardData对象。为了确保跨浏览器兼容性,最好只在发生剪贴板事件期间使用这个对象。

这个clipboardData对象有三个方法:getData()setData()clearData()。其中,getData()用于从剪贴板中取得数据,它接受一个参数,即要取得的数据的格式。在IE中,有两种数据格式:text和URL。在Firefox、Safari和Chrome中,这个参数是一种MIME类型;不过,可以用text代表text/plain

类似地,setData()方法的第一个参数也是数据类型,第二个参数是要放在剪贴板中的文本。对于第一个参数,IE照样支持text和URl,而Safari和Chrome仍然只支持MIME类型。但是,与getData()不同的是,Safari和Chrome的setData()不能识别text类型。这两个浏览器在成功将文本放到剪贴板中后,都会返回true;否则返回false。为了弥补这些差异,我们可以向EventUtil中再添加下列方法。

js
var EventUtil = { // ... getClipboardText: function (event) { var clipboardData = event.clipboardData || window.clipboardData; return clipboardData.getData('text'); }, // ... setClipboardText: function (event) { if (event.clipboardData) { return event.clipboardData.setData('text/plain', value); } else if (window.clipboardData) { return window.clipboardData.setData('text', value); } }, // ... }

在需要确保粘贴到文本框中的文本中包含某些字符,或者符合某种格式要求时,能够访问剪贴板是非常有用的。例如,如果一个文本框只接受数值,那么就必须检测粘贴过来的值,以确保有效。

js
EventUtil.addHandler(textbox, 'paste', function (event) { event = EventUtil.getEvent(event); var text = EventUtil.getClipboardText(event); if (!/^\d*$/.text(text)) { EventUtil.preventDefault(event); } });

由于并非所有浏览器都支持访问剪贴板,所以更简单的做法是屏蔽一个或多个剪贴板操作。

自动切换焦点

(略)

HTML5约束验证API

为了将表单提交到服务器之前验证数据,HTML5新增了一些功能。有了这些功能,即使JavaScript被禁用或者由于种种原因未能加载,也可以确保基本的验证。当然,这个功能只有在支持HTML5这部分内容的浏览器中才有效,这些浏览器有Firefox4+、Safari5+、Chrome和Opera10+。

必填字段

第一种情况是在表单中指定了required属性。这个属性适用于<input><textarea><select>字段(Opera11及之前版本还不支持<select>required属性)。

html
<input type="text" name="username" required>

在JavaScript中,通过对应的required属性,可以检测某个表单字段是否为必填字段。

js
var isUsernameRequired = document.forms[0].elements['username'].required;

另外,使用下面这代码可以测试浏览器是否支持required属性

js
var isRequiredSupported = 'required' in document.createElement('input');

其他输入类型

HTML5为<input>元素type属性又增加了几个值。这些新的类型不仅能反映数据类型的信息,而且还能香袋一些默认的验证功能。其中,emailurl是两个得到支持最多的类型,各浏览器也为它们增加了定制的验证机制。

html
<input type="email" name="email"> <input type="url" name="url">

email类型要求输入的文本必须符合电子邮件地址的模式,而url类型要求输入的文本必须符合URL的模式。

要注意的是,这些验证都会存在各种问题。而且,如果不给<input>元素设置required属性,那么空文本框也会验证通过,另一方面设置特写的输入类型并不能阻止用户输入无效的值,只是应用某些默认的验证而已。

数值范围

除了emailurl,HTML5还定义了另外几个输入元素。这几个元素都要求填写某种基于数字的值:numberrangedatetimedatetime-localdatemonthweek,还有time。浏览器对这几个类型的支持情况并不好,因此如果真想选用的话,要特别小心。

对所有这些数值类型的输入元素,可以指定min(最小的可能值)、max(最大的可能值)和step(从min到max的两个刻度间的差值)属性。例如,想让用户只能输入0到100的值,而且这个值必须是5的倍数,可以这样:

html
<input type="number" min="0" max="100" step="5" name="count">

以上这些属性在JavaScript中都能通过对应的元素访问(或修改)。此外,还有两个方法:stepUp()stepDown(),都接收一个可选的参数:要在当前值基础上加上或减去的数值。(默认是加或减1)这两个方法还没有得到任何浏览器支持。

输入模式

HTMl5为文本字段新增了pattern属性。这个属性的值是一个正则表达式,用于匹配文本框中的值。例如,如果只想允许在文本字段中输入数值:

html
<input type="text" prttern="\d+" name="count">

注意,模式的开头和末尾不用加^$符号(假定已经有了)。这两个符号表示输入的值必须从头到尾都与模式匹配。

与其他输入类型相似,指定pattern也不能阻止用户输入无效的文本。

在JavaScript中可以通过pattern属性来访问模式。

js
var pattern = document.forms[0].elements['count'].pattern;

检测有效性

使用checkValidity()方法可以检测表单中的某个字段是否有效。所有表单字段都有这个方法,如果字段的值有效,这个方法返回true,否则返回false。

js
if (document.forms[0].elements[0].checkValidity()) { // 字段有效 } else { // 字段无效 }

要检测整个表单是否有效,可以在表单自身调用checkValidity()方法。

checkValidity()方法简单地告诉你字段是否有效相比,validity属性则会告诉你为什么字段有效或无效。这个对象中包含一系列属性,每个属性会返回一个布尔值。

  • customError:如果设置了setCustomValidity(),则为true,否则返回false。
  • patternMinsmatch:如果值与指定的pattern属性不匹配,返回true。
  • rangeOverflow:值比max值大,返回true。
  • rangeUnderflow:值比min值小,返回true。
  • stepMisMatch:min和max之间的步长值不合理,返回true。
  • tooLong:超出maxlength属性指定的长度,返回true。
  • typeMismatch:值不是type要求的格式,返回true。
  • valid:如果这里的其他属性都是false,返回true。
  • valuMissing:如果标注为required的字段中没有值,返回true。

禁用验证

通过调用novalidate属性,可以告诉表单不进行验证。

html
<form method="post" action="sign.php" novalidate> <!-- 表单元素 --> </form>

在JavaScript中使用noValidate属性可以取得或设置这个值,如果这个属性存在,值为true,否则为false。

js
document.forms[0].noValidate = true; // 禁用验证

如果一个表单中有多个提交按钮,为了指定点击某个提交按钮不必验证表单,可以在相应的按钮上添加formnovalidate属性

选择框脚本

选择框是通过<select><option>元素创建的。为了方便与这个控件交互,除了所有表单字段共有的属性和方法外,HTMLSelectElement类型还提供了下列属性和方法。

  • add(newOption, relOption):在relOption之前插入新的option元素。
  • multiple:布尔值,表示是否允许多项选择。
  • options:控件中所有option元素的HTMLCollection。
  • remove(index):移除给定位置的选项。
  • selectedIndex:选中项的索引值,如果没有选中项,则为-1。对于多选,只保存第一项。
  • size:选择框中可见的行数。

选择框的type属性不是select-one就是select-multiple,这取决于HTML代码中有没有mutiple特性。选择框的value属性由当前选中项决定,相应规则如下:

  • 如果没有选中项,则保存空字符串。
  • 如果有一个选中项,则为该项的value特性值,哪怕是空字符串。
  • 如果有一个选中项,但该项没有value特性,则为该项的文本。
  • 如果有多个选中项,则依据前两条规则取得第一个选中的值。
js
var selectbox = document.forms[0].elements['selectbox']; var text = selectbox.options[0].text; var value = selectbox.options[0].value;

选择选项

对于只允许选择一项的选择框,访问选中项的最简单方式,就是使用选择框的selectedIndex属性

js
var selectedOption = selectbox.options[selectbox.selectedIndex];

另一种方式,就是取得对某一项的引用,然后将其selected属性设置为true。

js
selectbox.options[0].selected = true;

实际上,selected属性的作用主要是确定用户选择了选择框中的哪一项。要取得所有选中的项,可以循环遍历选项集合,然后测试每个选项的selected属性

js
function getSelectedOptions (selectbox) { var result = new Array(); var option = null; for (var i = 0, len = selectbox.options.length; i < len; i++) { option = selectbox.options[i]; if (option..selectd) { result.push(option); } } return result; }

添加选项

可以使用JavaScript动态创建选项,并将它们添加到选择框中。添加选项的方式有很多,第一种方式就是使用DOM方法。

js
var newOption = document.createElement('option'); newOption.appendChild(document.createTextNode('新选项')); newOption.setAttribute('value', 'Option value'); selectbox.appendChild(newOption);

第二种方式就是使用Option构造函数来创建新选项。这个构造函数接受两个参数:text和value;第二个参数是可选的。

js
var newOption = new Option('新选项', 'Option value'); selectbox.appendChild(newOption); // 有IE8及之前版本中有问题。

第三种方法就是使用选择框的add()方法。这个方法接受两个参数:要添加的新选项和位于新选项之后的选项(传入undefined默认添加到最后)。

js
var newOption = new Option('新选项', 'Option value'); selectbox.add(newOption, undefined); // 最佳方案

移除选项

首先,可以使用DOM的removeChild()方法。

其次,可以使用选择框的remove()方法。这个方法接受一个参数,即要移除选项的索引。

js
selectbox.remove(0); // 移除第一个选项

最后一种方式就是将相应选项设置为null。

js
selectbox.options[0] = null; // 移除第一个选项

移动和重排

我们可以使用DOM的appendChild()方法,就可以将第一个选择框中的选项直接移动到第二个选择框中。我们知道,如果为appendChild()方法传入一个文档中已有的元素,那么就会先从该元素的父节点中移除它,再把它添加到指定的位置。

js
var selectbox1 = document.getElementById('selectbox1'); var selectbox2 = document.getElementById('selectbox2'); selectbox2.appendChild(selectbox1.options[0]);

移动选项与移除选项有一个共同之处,即会重置每一个选项的index属性

重排选项次序的过程也十分类似,使用DOM方法的insertBefore()

js
var optionToMove = selectbox.options[1]; selectbox.insertBefore(optionToMove, selectbox.options[optionToMove.index - 1]);

表单序列化

随着Ajax的出现,表单序列化已经成为一种常见需求。在JavaScript中,可以利用表单字段的type属性,连同namevalue属性一起实现对表单的序列化。在编写代码之前,必须先搞清楚在表单提交期间,浏览器是怎样将数据发送给服务器的:

  • 对表单字段的名称和值进行URL编码,使用(&)分隔。
  • 不发送禁用的表单字段。
  • 只发送勾选的复选框和单行按钮。
  • 不发送type为resetbutton的按钮。
  • 多选选择框的每个选中的值单独一个条目。
  • 在单击提交按钮提交表单的情况下,也会发送提交按钮;否则,不发送提交按钮。也包括type为image的input元素。
  • select元素的值,就是选中的option元素的value特性的值。如果option元素没有value特性,则为文本值。

在表单序列化过程中,一般不包含任何按钮字段,因为结果字符串很可能是通过其他方式提交的。除此之外的其他上述规则都应该遵循。以下就是实现表单序列化的代码。

js
function serialize (form) { var parts = [], filed = null, i, len, j, optLen, option, optValue; for (i = 0, len = form.elements.length; i < len; i++) { filed = form.elements[i]; switch (filed.type) { case 'select-one': case 'select-multiple': if (filed.name.length) { for (j = 0, optLen = filed.options.length; j < optLen; j++) { option = filed.options[j]; if (option.selected) { optValue = ''; if (option.hasAttribute) { optValue = option.hasAttribute('value') ? option.value : option.text; } else { optValue = option.attributes['value'].specified ? option.value : option.text; } parts.push(encodeURIComponent(filed.name) + '=' + encodeURIComponent(optValue)); } } } break; case undefined: // 字段集 case 'filed': // 文件输入 case 'submit': // 提交按钮 case 'reset': // 重置按钮 case 'button': // 自定义按钮 break; case 'radio': case 'checkbox': if (!filed.checked) break; default: // 不包含没有名字的表单字段 if (filed.name.length) { parts.push(encodeURIComponent(filed.name) + '=' + encodeURIComponent(optValue)); } } } return parts.join('&'); }

富文本编辑

富文本编辑,又称为WYSIWYG(What You See Is What You Get,所见即所得)。在网页中编辑富文本内容,是人们对Web应用程序最大的期待之一。虽然也没有规范,但在IE最早引入了这一个功能的基础上,已经出现了事实标准。而且,Opera、Safari、Chrome和Firefox都已经支持这一功能。这一技术的本质,就是在页面中嵌入一个包含空HTML页面的iframe。通过设置designMode属性,这个空白的HTML页面可以被编辑,而编辑对象是该页面<body>元素的HTML代码。designMode有两个可能的值:off(默认值)和on。在设置为on时,整个文档就会应得可以编辑。

使用contenteditable属性

另一种编辑富文本内容的方式就是使用名为contenteditable的特殊属性,这个属性也是由IE最早实现的。可以把contenteditable属性应用给页面中任何元素,然后用户立即就可以编辑该元素。

html
<div class="editable" id="richedit" contenteditable></div>

通过在这个元素上设置contenteditable属性也能打开或关闭编辑模式。这个属性有三个可能的值:"true"表示打开、"false"表示关闭、"inherit"表示从父元素那里继承。支持ceontenteditable属性的浏览器在IE、Firefox、Chrome、Safari和Opera。在移动设备上,有IOS5+中的Safari和Android3+中的WebKit。

js
var div = document.getElementById('richedit'); div.contenteditable = 'true';

操作富文本

与富文本编辑器交互的主要方式,就是使用document.execCommand()。这个方法可以对文档执行预定义的命令,而且可以应用大多数格式。可以为document.execCommand()传递3个参数:要执行的命令名称、表示浏览器是否应该为当前命令提供用户界面的一个布尔值和执行命令必须的一个值(如果不需要值,则传递null)。为了确保跨浏览器的兼容性,第二个参数应该始终设置为false。

不同浏览器的预定义命令也不一样。下表列出了那些被支持最多的命令。

命令 值(第三个参数) 说明
background 颜色字符串 设置文档的背景颜色
bold null 加粗文本
copy null 复制文本
createlink URL字符串 将选中的文本转换成链接,指向指定的URL
cut null 剪切
delete null 删除
fontname 字体名称 设置字体
fontsize 1-7 设置字体大小
forecolor 颜色字符串 设置文本颜色
formalblock 要包围当前文本块的HTML标签:如<h1> 使用指定标签格式化选中文本
indent null 缩进
inserthorizontalrule null 插入一个<hr>
insertimage 图像的URL 插入一个图像
insertorderedlist null 插入一个有序列表<ol>
insertunorderedlist null 插入一个无序列表<ul>
insertparagraph null 插入一个<p>
italic null 斜体
justifycenter null 将插入光标所在的文本块居中对齐
justifyleft null 左对齐
outdent null 减少缩进
paste null 粘贴
removeformat null 撤销formatblock命令的操作
selectall null 选中文档中所有文本
underline null 下划线
unlink null 移除链接,撤销createlink命令的操作

其中,与剪贴板有关的命令在不同浏览器中差异很大。

示例:

js
frames['richedit'].document.execCommand('bold', false, null);
js
// 页面中contenteditable属性为true的区块 document.execCommand('bold', false, null);

除了命令之外,还有一些与命令相关的方法。

queryCommandEnabled()用于检测是否可以针对当前选择的文本,或者当前插入字符所在位置执行某个命令。允许返回true,否则返回false。

js
var result = frames['richedit'].document.queryCommandEnabled('bold');

另外,queryCommandState()方法用于确定是否已将指定命令应用到了选择的文本。

js
var isBold = frames['richedit'].document.queryCommandState('bold');

最后一个方法是queryCommandValue(),用于取得执行命令时传入的值(即document.execCommand()的第三个参数)。

js
var fontSize = frames['richedit'].document.queryCommandValue('fontsize');

富文本选区

在富文本编辑器中,使用框架(iframe)的getSelection()方法,可以确定实际选择的文本。这个方法是window对象和document对象的属性,调用它会返回一个表示当前选择文本的Selection对象。每个Selection对象都有下列属性:

  • anchorNode:选区起点所在的节点。
  • anchorOffset:在到达选区起点位置之前跳过的anchorNode中的字符数量。
  • focusNode:选区终点的所在的节点。
  • focusOffset:focusNode中包含在选区之内的字符数量。
  • isCollapsed:布尔值,表示选区的起点和终点是否重合。
  • rangeCount:选区中包含的DOM范围的数量。

Selection对象的这些属性并没有包含多少有用的信息。好在,该对象的下列方法提供了更多信息,并且支持对选区的操作:

  • addRange(range):将指定的DOM范围添加到选区中。
  • collapse(node, offset):将选区折叠到指定节点中相应的文本偏移位置。
  • collapseToEnd():将选区折叠到终点位置。
  • collapseToStart():将选区折叠到起点位置。
  • containsNode(node):确定指定的节点是否包含在选区中。
  • deleteFromDocument():从文档中删除选区中的文本。
  • extend(node, offset):通过将focusNode和focusOffset移动到指定的值来扩展选区。
  • getRangeAt(index):返回索引对应的选区中的DOM范围。
  • removeAllRanges():从选区中移除所有DOM范围。实际上,这样会移除选区。
  • removeRange():从选区中移除指定的DOM范围。
  • selectAllChildren(node):清除选区并选择指定节点的所有子节点。
  • toString():返回选区所包含的文本内容。

Selection对象的这些方法都非常实用,它们利用了DOM范围来管理选区。由于可以直接操作选择文本的DOM表现,因此访问DOM范围与使用execCommand()相比,能够对富文本编辑器进行更加细化的控制。

js
var selection = frames['richedit'].getSelection(); // 取得选择的文本 var selectedText = selection.toString(); // 取得代表选区的范围 var range = selection.getRangeAt(0); // 突出显示选择的文本 var span = frames['richedit'].document.createElement('span'); span.style.backgroundColor = 'yellow'; range.surroundContents(span);

HTML5将getSelection()方法纳入了标准,而且IE9、Firefox、Safari、Chrome和Opera8都实现了它。

表单与富文本

由于富文本是使用iframe而非表单控件实现的,因此从技术上说,富文本编辑器并不属于表单。换句话说,富文本编辑器中的HTML不会被自动提交给服务器,需要我们手工提取并提交。为此,可以添加一个隐藏的表单字段,让它的值等于从iframe中提取出来的HTML。

js
EventUtil.addHandler(form, 'submit', function(event) { event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); target.elements['comments'].value = frames['richedit'].document.body.innerHTML; // 对于contenteditable元素,可以使用下面的方式 target.elements['comments'].value = document.getElementById('richedit').innerHTML; });