0%

JavaScript学习笔记(14)

《JavaScript高级程序设计》第十四章,表单脚本。


基本行为

可能要先说一下<form>这个元素了,它对应的JavaScript对象类型是HTMLFormElement,这种类型有下面这么些属性方法。

  • acceptCharset:服务器能够处理的字符集,同HTML里的accept-charset
  • enctype:请求的编码类型,同HTML里的enctype
  • action:接收请求的URL,同HTML里的action
  • method:发送的请求类型(POSTGET等),同HTML里的method
  • name:表单名称,同HTML里的name
  • elements:表单元素的集合
  • length:表单里元素的数量
  • reset():重置表单
  • submit():提交表单

获取表单

可以常规地根据id获取表单,也可以用document.forms[index/"name"]来获取表单。

提交表单

type设置为"submit"<input>/<button>在被点击/回车时会提交表单,此时会先触发submit事件、再将其发往服务器。借用submit的事件处理函数,我们可以对表单数据进行校验,以决定是否允许表单提交,不允许则event.preventDefault()。不能用onclick,因为clicksubmit的顺序在不同浏览器之间有区别。

也可以在代码中使用form.submit()来提交表单,但这不会触发submit事件。

提交表单要注意的就是重复提交了,解决办法有:按钮点击一次即置灰、在onsubmit里取消后续的提交操作。

重置表单

type设置为"rest"<input>/<button>在被点击时会重置表单,此时会先触发reset事件、再将各字段重置(或为空或为默认值)。也可以借用reset的事件处理函数去取消默认行为。

也可以在代码中使用form.reset()来重置表单,这里就会触发reset事件了。

表单字段

使用form.elements[index/"name"]来获取表单里的元素,如果用name来获取且同name的元素有多个,那就会返回一个NodeList

表单里的字段无非是些输入框、单选框、复选框等等,那么就很容易想到它们会有哪些属性了,列举如下:form(指向所属表单的指针)、namevaluetypetabIndexdisabledreadOnlyautofocus(自动聚焦),这些属性除了第一个外都是可以改写的。

思考一下,也很容易想到它们大概拥有的方法:focus()blur()

这些元素也会和一些事件相关,结合上面可以想到有focusblur事件,而除了这俩还有一个change事件,表示元素的value变化了。

下面就来关注一下表单中常见的元素。

文本框

表示文本框

1
2
3
4
<!--   ⬇必需       ⬇可显示的字符数    ⬇可接受的字符数 -->
<input type="text" size="25" maxLength="50" value="Default value">
<!-- ⬇默认值必须放这里 -->
<textarea rows="25" cols="5">Default value</textarea>

选择文本

可以使用select()方法选择文本框中的所有文本,一般的使用场景为文本框获得焦点后全选,这样方便用户去删除或复制。

1
2
3
textbox.addEventListener("focus", (e) => {
e.target.select();
});

select()方法对应的是select事件,这个事件会在选择文本框的文本时触发,不过具体时序因浏览器而异。

选择好文本之后,可以通过元素的selectionStartselectionEnd来明确选中的文本起始和结束处的偏移量,可以用textbox.value.substring(textbox.selectionStart, textbox.selectionEnd)来获取内容。

还可以使用setSelectionRange()方法来选择部分文本,接收两个参数:起始字符下标、结束字符下标后一个。

屏蔽字符

字符输入是keypress的结果,因此可以在其事件处理函数中屏蔽特定字符的输入,下面是个只允许输入数值的例子。

1
2
3
4
5
6
7
8
textbox.addEventListener("keypress", (e) => {
const target = e.target;
const charCode = e.getCharCode(e);
// 屏蔽非数值字符、不屏蔽基本按键(向上、向下、推个、删除等)、不屏蔽ctrlcv
if(!/\d/.test(String.fromCharCode(charCode)) && charCode > 9 && !e.ctrlKey) {
e.preventDefault();
}
});

操作剪贴板

有六个剪贴板事件:beforecopycopybeforecutcutbeforepastepaste。不带before的事件在剪贴板进行操作时发生,带before的事件则通常用于向剪贴板发送数据、或者从剪贴板取得数据。

可以通过clipboardData属性拿到剪贴板数据。不过不同浏览器有区别,比如挂到不同的对象上、可访问性不一样等,为了兼容最好只在发生剪贴板事件时使用此对象。这个clipboardData对象有三个方法:getData(type)setData(type, value)clearData()。这里的type都表示数据格式,在IE中有"text""URL",在Firefox/Safari/Chrome中有"text/plain"

自动切换焦点

一般的情景是,用户填完既定长度的数据后,表明这个字段填完了,随即自动切换焦点。下面举个这样的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function() {
function tabForward(e) {
const target = e.target;
if(target.value.length == target.maxLength) {
const form = target.form;
for(let i = 0; i < form.elements.length; i++) {
if(form.elements[i] == target) {
if(form.elements[i + 1]) {
form.elements[i + 1].focus();
}
return;
}
}
}
}

textbox1.addEventListener("keyup", tabForward);
textbox2.addEventListener("keyup", tabForward);
textbox3.addEventListener("keyup", tabForward);
})();

HTML5约束验证API

一种自动验证的约束,包括:

  • 对含有required的必填约束
  • 类型为"email""URL"的内容匹配约束
  • 类型为"number""range""datetime""datetime-local""date""month""week""time"时,指定的minmaxstep等约束
  • 含有pattern的正则匹配约束(如在HTML中加上pattern="\d+")
  • 对含有noValidate的表单/含有formnovalidate的字段不需验证约束(当我不需要约束时,这种不约束也算一种约束hh)

根据上面的这些约束,可以定义字段是否「有效」。下面是判断有效性的两种方法:

  • checkValidity()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 用于单个字段

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

// 用于整个表单

if(document.forms[0].checkValidity()) {
// 表单有效,继续
} else {
// 表单无效
}
  • validity属性
    它会告诉我们为什么字段有效或无效。这个对象包含一系列属性,每个属性都是布尔值:
    • customError:如果设置了setCustomValidity()则为true,否则为false
    • patternMismatch:值与pattern不匹配
    • rangeOverflow:值比max
    • rangeUnderflow:值比min
    • stepMismatchminmax之间步长设置不合理
    • tooLong:值长度超过maxLength
    • typeMismatch:值不是"mail""url"要求的格式
    • valid:当其他属性都是false时为true,否则为false
    • valueMissingrequired的值无内容
1
2
3
4
5
6
7
8
9
if(input.validity && !input.validity.valid) {
if(input.validity.valueMissing) {
alert("Please specify a value");
} else if(input.validity.typeMismatch) {
alert("Please enter the correct stuff");
} else {
alert("Value is invalid");
}
}

选择框

选择框由<select><option>组成。

对于<select>,有几个属性:valuemultipleoptionssize(选择框中可见的行数)、selectedIndex

对于<option>,有几个属性:indexlabeltextvalueselected

获取选项

可以用options获取所有选项,用selectedIndex获取当前选中项的下标(即使是多选,这个属性也只有一个下标,值为所有选中项中第一个的下标),用value获取这个选择框的值。

value的内容有这么些规则:

  • 如果没有选中的项,则value""
  • 如果有一个选中项,且该项value已在HTML中指定,则value为该项的value
  • 如果有一个选中项,且该项value未在HTML中指定,则value为该项的文本
  • 如果由多个选中项,则value为根据前面两条规则获取的第一个选中项的值

举个例子如下,当选第一项,值为"a";当选第四项,值为"";当选第五项,值为"E"

1
2
3
4
5
6
7
<select name="location">
<option value="a">A</option>
<option value="b">B</option>
<option value="c">C</option>
<option value="">D</option>
<option>E</option>
</select>

选择选项

可以指定selectedIndex来选择选项(对于多选,每设置一次都会取消之前的选项并选择这次的选项),也可以将选项的selected设置为true来选择选项(对于多选,这回是真的可以多选了)。

添加选项

  • DOM操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 使用createElement创建
    let newOption = document.createElement("option");
    newOption.appendChild(document.createTextNode("Option text"));
    newOption.setAttribute("value", "Option value");
    selectbox.appendChild(newOption);

    // 使用`Option`构造函数来创建
    let newOption = new Option("Option text", "Option value");
    selectbox.appendChild(newOption);
  • add()方法
    此方法接收两个参数:要添加的选项、将位于新选项之后的选项。IE中第二个参数可选,且该内容为索引;DOM中第二个参数则为必填项。
    1
    2
    let newOption = new Option("Option text", "Option value");
    selectbox.add(newOption, undefined);

移除选项

  • DOM操作

    1
    selectbox.removeChild(selectbox.options[0]);
  • remove()方法
    此方法接收一个参数:要移除选项的索引。

    1
    selectbox.remove(0);
  • 置空
    还可以将相应选项设置为null来移除选项。

    1
    selectbox.options[0] = null;

要注意的是,移除选项后会自动重置每一个选项的index属性。

移动选项

就要借助DOM操作啦,举几个例子。

1
2
3
4
5
// 把第一个选择框的某选项直接移到第二个选择框处
selectbox2.appendChild(selectbox1.options[0]);

// 把某个选项向前移动一个位置
selectbox.insertBefore(optionToMove, selectbox.options[optionToMove.index - 1]);

移动选项也会自动重置每一个选项的index属性。

表单序列化

有一些原则,比如:

  • 各个字段名称和值进行URL编码,用&分隔
  • 不发送禁用的字段、类型为"reset""button"的按钮
  • 只发送勾选的复选框和单选框
  • 复选框中,每个选中的值单独一个条目
  • 单选框中,选项有value则为该值,无则为文本值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function serialize(form) {
const parts = [];

for(let i = 0; i < form.elements.length; i++) {
let field = form.elements[i];

switch(field.type) {
case "select-one":
case "select-multiple":
if(field.name.length) {
for(let j = 0; j < field.options.length; j++) {
let option = field.options[j];
if(option.selected) {
let value = "";
if(option.hasAttribute) {
// DOM
value = option.hasAttribute("value")
? option.value
: option.text;
} else {
// IE
value = option.attributes["value"].specified
? option.value
: option.text;
}
parts.push(`${encodeURLComponent(field.name)}=${encodeURLComponent(field.value)}`);
}
}
}
break;

case undefined: // 针对`fieldset`
case "file": // 文件上传
case "submit": // 提交按钮
case "reset": // 重置按钮
case "button": // 自定义按钮
break;

case "radio": // 单选框
case "checkbox": // 复选框
if(!field.checked) {
break;
}

default:
if(field.name.length) {
parts.push(`${encodeURLComponent(field.name)}=${encodeURLComponent(field.value)}`);
}
}
}
return parts.join("&");
}

富文本编辑

据说又叫「所见即所得」,其实编辑邮件正文那块儿就是一种。

创建区域

  • 使用iframe
    一种方法是,在页面中嵌入一个包含空HTML的iframe,通过将它的designMode设置为"on"(默认为"off"),这个空白HTML页面可以被编辑(显示插入符号),而编辑对象则是该页面<body>元素的HTML代码。要注意的是,只有在页面完全加载之后才能设置designMode,因此可以把它写在onload里。

  • 使用contenteditable
    还有一种方式,在元素的HTML中设置contenteditable。这样,元素包含的任何文本内容都可以编辑了,就好像这个元素变成了<textarea>(试了一下,确实会出现插入符号,也可以输入内容)。contenteditable有三个值:"true""false""inherit"

操作富文本

与富文本编辑器交互的主要方式,就是使用frame["x"].document.execCommand()document.execCommand()。它可以对文档执行预定义的命令,接收三个参数:命令名称、表示浏览器是否应该为当前命令提供用户界面的一个布尔值、执行命令必须的一个值(如不需要值则传null)。下面列举一些命令及相应的第三个参数(没写的则是不需要参数的null):

  • backcolor:参数为颜色字符串,设置文档背景色
  • bold:将选择的文本加粗
  • italic:将选择的文本转为斜体
  • underline:将选择的文本添加下划线
  • copy:将选择的文本复制到剪贴板
  • paste:将剪贴板文本粘贴到选择的文本
  • cut:将选择的文本剪切到剪贴板
  • createLink:参数为URL字符串,将选择的文本转换成链接,指向特定URL
  • unlink:移除文本链接,撤销createlink
  • indent:缩进文本
  • outdent:凸排文本
  • formatblock:参数为要包围当前文本块的HTML标签(如<h1>),用给定的标签格式化选择的文本
  • removeformat:删除格式,撤销formatblock
  • fontname:参数为字体名称,给选择的文本指定字体
  • fontsize:参数为1~7,给选择的文本指定字号
  • fontcolor:参数为颜色字符串,给选择的文本指定颜色
  • inserthorizontalrule:在插入字符处插入一个<hr>元素
  • insertimage:参数为图片URL,在插入字符处插入一个图像
  • insertorderlist:在插入字符处插入一个<ol>元素
  • insertunorderlist:在插入字符处插入一个<ul>元素
  • insertparagraph:在插入字符处插入一个<p>元素
  • justifycenter:文本块居中对齐
  • justifyleft:文本块居左对齐
  • selectall:选择所有文本
  • delete:删除选择的文本

queryCommandEnabled()方法,可以用来检测针对当前选择的文本或当前插入字符所在位置,是否适合执行某个命令,会返回表示是否合适的布尔值。如下代码,可以看看能否对当前选择的文本执行bold

1
const result = frames["richedit"].document.queryCommandEnabled("bold");

queryCommandState()方法,可以用来检测是否已将指定指令应用到了选择的文本,也是返回布尔值。如下代码,可以看看当前选择的文本是否已经转换为粗体:

1
const isBold = frames["richedit"].document.queryCommandState("bold");

queryCommandValue()方法,可以取得执行命令时传入的参数(即execCommand()的第三个参数)。如下代码,如果文本在应用fontsize命令时传入了7,那就会返回"7"

1
const fontSize = frames["richedit"].document.queryCommandValue("fontsize");

富文本选区

也就是为了更精确地控制选中的内容。用windowdocument对象的getSelection()方法可以确定实际选择的文本,它会返回一个Selection对象。每个Selection对象都有下面的属性和方法。

属性:

  • anchorNode:选区起点所在节点
  • anchorOffsetanchorNode中排除在选区之外的字符数量(也就是从最开始到选区起点经过的字符数量)
  • focusNode:选区终点所在节点
  • focusOffsetfocusNode中包含在选区之内的字符数量
  • isCollapsed:选区起点和终点是否重合
  • rangeCount:选区包含的DOM范围的数量

方法:

  • addRange(range):将指定的DOM范围添加到选区中
  • removeRange(range):从选区中移除指定的DOM范围
  • removeAllRanges():从选区中移除所有DOM范围,这样会移除选区(因为选区至少要有一个范围)
  • getRangeAt(index):返回索引对应的选区的DOM范围
  • deleteFromDocument():从文档中删除选区中的文本,同document.execCommand("delete", false, null)
  • collapse(node, offset):将选区折叠到指定节点中的相应的文本偏移位置
  • collapseToEnd():将选区折叠到终点位置
  • collapseToStart():将选区折叠到起点位置
  • containsNode(node):指定节点是否包含在选区中
  • extend(node, offset):通过移动focusNodefocusOffset扩展选区
  • selectAllChildren(node):清除选区并选择指定节点的所有子节点
  • toString():返回选区包含的文本内容

举一个操作的例子,这里给富文本编辑器中被选择的文本添加黄色的背景:

1
2
3
4
5
6
7
8
9
10
const selection = frames["richedit"].getSelection();
// 获取选择的文本
const selectedText = selection.toString();
// 获取代表选区的范围
const range = selection.getRangeAt(0);

// 突出表示选择的文本
const span = frames["richedit"].document.createElement("span");
span.style.backgroundColor = "yellow";
range.surroundContents(span);

* 表单与富文本

在严格意义上来说,富文本编辑器不属于表单,因此其中的内容不会被自动提交给服务器,因此需要手动提取并提交。通常可以添加一个隐藏的表单字段,让它的值等于从iframe中/contenteditable元素中的HTML,再提交给服务器。

1
2
3
4
5
6
7
8
form.addEventListener("submit", (e) => {
// 使用iframe
e.target.elements["comments"].value = frames["richedit"].document.body.innerHTML;
// 使用contenteditable
e.target.elements["comments"].value = document.getElementById("richedit").innerHTML;

...
})