客户端javascript存在使得静态的html文档编程了交互式的web应用。校本化web页面内容是javascript的核心目标。本章———本书最重要的章节之一,阐述了它是如何做到的客户端javascript的存在使得静态的html文档变成了交互式的web应用。校本化web页面javascript核心目标。本章将阐述它是如何做到的。

第11章和12章解释了每一个web浏览器窗口、标签也和框架由一个window对象所示。每个window对象有一个document对象,document对象表示窗口的内容,它就是本章的主题。尽管如此,Document对象并非独立的,它是一个巨大的API的核心对象,叫做文档对象模型(Document Object Model ,DOM),它代表和操作文档的内容

本章开始部分解释DOM的基本框架,然后进一步解释以下内容:

  • 如何在文档中查询或选取单独元素
  • 如何将文档作为节点树来遍历,如何找到文档元素的祖先、兄弟和后代元素
  • 如何查询和设置文档的元素的属性
  • 如何查询、设置和修改文档内容
  • 如何通过创建、插入和删除节点来修改文档结构
  • 如何与html表单一起工作

本章最后一节涵盖其它各种文档特性,包括referrer属性、write()方法和查询当前文档中选取的文档文本的技术等

1.DOM概览

文档对象模型(DOM)是表示和操作HTML和XML文档内容的基础API。API不是特别复杂,但是需要了解大量的构架细节。首先:应该理解HTML或XML文档的嵌套元素在DOM树对象中的表示。HTML文档树的结构包含表示HTML标签或元素(如body,<p>)和表示文本字符串的字节,它也可能包含HTML注释的节点。考虑一下简单的HTML文档:

            <html>
                <head>
                    <title>title name</title>
                </head>
                <body>
                    <h1>an html Document</h1>
                    <p>this is a <i>simple</i>docuent</p>
                </body>
            </html>    

此文档DOM,HTML文档的树状表示:

 如果还未熟悉计算机编程中的树状结果,借用家谱图形容是比较有用的方法。在一个节点之上的直接节点是其父节点,在其下一层的直接节点是其子节点。在同一层上具有相同父节点的是兄弟及诶单。在一个节点之下的所有层级的一组节点是其后代节点。一个节点任何父节点和其上层的所有节点是祖先节点

上图的每个方框是文档的一个节点,它表示一个Node对象,我们将在后续几节中讨论Node的属性和方法, 并且可以在第四部分查到这些属性和方法。上图中包含三种类型不同的节点。树行的根部是Document节点,它代表整个文档代表HTML元素的节点是Element节点代表文本的节点是Text节点。Document、Element和Text是Node的子类,在第四部分中它们有自己的条目。Document和Element是两个重要的DOM类,本章大部分内容将删除它们的属性和方法。

下图展示了Node及其在类型层次结构中的子类型。注意,通用的Document和Element类型与HTMLDocument和HTMLElement类型之间有严格的区别。Document类型代表一个HTML或XML文档 ,Element类型代表了该文档中的一个元素。HTMLDocument和HTMLElement子类只是征对于HTML文档和元素。此书中,我们经常使用类名Document和Element,甚至在指代HTML文档时也不例外。在第四部分也是如此:HTMLDocument和HTMLElement类型 属性和方法记录于Document和Element参考页中

上文的每个方框是文档的一个节点,它表示一个Node对象,我们将在后续几节中讨论Node的属性和方法, 并且可以在第四部分查到这些属性和方法。上图中包含三种类型不同的节点。树行的根部是Document节点,它代表整个文档。 代表HTML元素的节点是Element节点,代表文本的节点是Text节点。Document、Element和Text是Node的子类,在第四部分中它们有自己的条目。Document和Element是两个重要的DOM类,本章大部分内容将删除它们的属性和方法。

下图展示了Node及其在类型层次结构中的子类型。注意,通用的Document和Element类型与HTMLDocument和HTMLElement类型之间有严格的区别。Document类型代表一个HTML或XML文档 ,Element类型代表了该文档中的一个元素。HTMLDocument和HTMLElement子类只是征对于HTML文档和元素。此书中,我们经常使用类名Document和Element,甚至在指代HTML文档时也不例外。在第四部分也是如此:HTMLDocument和HTMLElement类型 属性和方法记录于Document和Element参考页中。

文档节点的部分层次结构:

值得注意的是,上图中HTMLElement的很多子类型代表HTML元素的具体类型。每个类型定义多个javascript属性,它们对应具体的元素或元素组,(本章4.i节)的HTML属性。有些具体元素也定义额外的属性和方法,它们并不是简单地映射HTML语法。第四部分涵盖这些类型及其额外的特性。

最后,上图还展示了到目前还未接触的一些节点类型,Conmment节点代表HTML或XML的注释。由于注释基本上是文本字符串,因此它们很像表示文档中显式文本的Text节点CharacterData通常是Text和Conmment的祖先,它定义这两种节点所共享的方法Attr节点类型代表XML或HTML属性,但它几乎从不使用,因为和文档节点不同,Element类型定义了将属性当做“名/值”对使用方法。DocumentFragment类(未在上图显式)在实际文档中并不存在的一种节点:它代表一系列没有常规父节点的节点,对一些文档操作来说DocumentFragment非常有用,本章6.iiii将涵盖这部分内容。DOM也定义了一些不常使用的类型,如像代表Doctype声明和xml处理指令等类型。

2.选取文档元素

大多数客户端javascript程序运行时总是在操作一个或多个文档元素,这些程序启动时,可以使用全局变量document来引用Document对象。但是,为了操作文档中的元素,必须通过某种方式来获得或选取这些文档元素的Element对象,DOM定义了很多方式来选取元素,查询文档的一个或多个元素有如下的方法:

  • 用指定的id属性
  • 用指定的name属性
  • 用指定的标签名字
  • 用指定的css类
  • 用匹配的css选择器

随后几节将解释每一种元素选取技术

i.通过id选取元素

任何HTML元素可以有一个id属性,在文档中值必须唯一,即同一个文档中两个元素不能有相同的ID。可以用Document对象的getElementById()方法选取一个基于唯一ID的元素,此方法我们在11章和12章都说明过了:

var section1 = document.getElementById("section1")

这就是最简单和常用的选取元素的方法。如果想要操作一组指定的文档元素,提供这些元素的id属性,并且使用ID查找这些Element对象。

如果需要通过ID查找多个元素,会发现下面的例子getElements()函数非常有用:

            /**
             * 函数接受任意多的字符串参数
             * 每个参数将当做元素的id传给document.getElementById()
             * 返回一个对象,它把这些id映射到对应的Element对象
             * 如任何一个id对应的元素未定义,则抛出一个Error对象
             **/
            function getElement( /*ID(s)*/ ) {
                var elements = {}; //开始是一个map映射对象
                for (var i = 0; arguments.length; i++) { //循环每个参数
                    var id = arguments[i]; //参数是元素的id
                    var elt = document.getElementById(id); //查找元素
                    if (elt == null)
                        throw new Error("No element with id: " + id); //抛出异常
                    elements[id] = elt; //id和元素之间的映射
                }
                return elements; //对于元素映射返回id
            }

在低于IE8版本的浏览器中,getElementById()对匹配元素不区分大小写,而且也返回匹配name属性的元素。

ii.通过name名字获取元素

HTML的name属性最初打算为表单元素分配名字,在表单数据提交到服务器时使用该属性的值。类似id属性,name是给元素分配的名字,但是区别id,name的属性值不是必须唯一:多个元素可能有同样的名字,在表单中,单选和复选按钮通常是这样的情况。而且和id不一样的是name属性值是在少数HTML元素中有效,包括表单、表单元素、<iframe>和<img>元素

基于name属性的值选取html元素,可以使用Document对象的getElementByName()方法。

            var radiobuttons = document.getElementsByName("favorite_color");

getElementsByName()定义在HTMLDocument类中,而不在Document类中,所有它只征对HTML文档可用,在xml文档中不可用。它返回一个NodeList对象,后者的行为类似若干Element对象的只读数组。在IE中,getElementByname()也返回id属性匹配中只读的元素。为了兼容,应该小心谨慎,不要将ID和name同名。

在12章7节中我们看到,为某些HTML元素设置name属性将自动为window对象中创建对于的属性,对Document对象也类似。为<form><img><iframe><applet><embed><object>元素(其中只有<object>元素没有后背对象)设置name属性值, 即在Document对象中创建以此name属性值为名字的属性。

如果给定的名字只有一个元素,自动创建的文档属性对于的值该是元素本身。如果有多个元素,该文档属性的值是一个NodeList对象,它表现为一个包含这些元素的数组。如12章7节所示,为若干命名<iframes>元素创建的文档属性比较特殊:它们指代这些框架的window对象而不是Element对象。这就意味这有些元素可以作为Document属性仅通过名字来选取:

                 //征对<form name="shipping">元素,得到Element对象
                var form = document.shipping;

在14章7节介绍了为什么不要用为窗口对象自动创建的属性,这同样适用用为文档对象自动创建的属性。如果需要查找命名的元素,最好显式地调用getElementByName()来查找它们。

iii.通过标签名选取元素

Document对象的getElementsByTagName()方法可用来选取指定类型(标签名)的所有HTML或XML元素。例如,如下代码,在文档中获得包含所有<span>元素的只读类数组对象。

                var spans = document.getElementsByTagName("span");

类似于getElementByName(),getElementByTagName()返回一个NodeList对象(关于NodeList类,见本节补充的信息)。在NodeList中返回的元素按照在文档中的顺序排序的,所有可用如下代码选取文档中的第一个p元素

                var firstspan = document.getElementsByTagName("span")[0];

<html>标签是不区分大小写的,在HTML文档中使用getElementsByTagName()时,它进行不区分大小写的标签名比较。例如,上述的变量span将包含所有写成<SPAN>的span标签。

给getElementByTagName()传递通配符参数"*"将获得一个代表文档中所愿元素的NodeList对象

Element定义getElementByTagName()方法,其原理和Document版本一样,但是它只选取调用该方法的元素的后代元素。因此,要查找文档的第一个<p>元素里所有<span>元素,代码如下:

            var firstpara = document.getElementsByTagName("p")[0];
            var firstParaSpan = firstpara.getElementsByTagName("span");

由于历史的原因,HTMLDocument类定义的一些快捷属性来访问各种各样的节点。例如images、forms和links等属性行为执行类似只读数组<img>、<from>和><a>(但只包含哪些有href属性的<a>标签)元素结合。这些属性指代HTMLCollection对象,它们很像NodeList对象,但是除此之外它们可以用元素的id或名字来索引,我们已经看到用法如下的表达式来索引一个命名的<form>元素:

document.shiping;

用document.forms属性也可以更具体地引用命名(或有ID)表单,如下:

document.forms.shipping

HTMLDocument对象还定义两个属性,它们指代包含特殊的单个元素而不是元素的集合。document.body是一个HTML文档的 <body>元素,document.head是<head>元素,浏览器隐式地创建它们。Document类的documentElement属性指代文档的根元素,在HTML文档中,它总指代<HTML>元素

节点列表和HTML集合

节点列表和HTML集合 getElementByName()和getElementByTagName()都返回NodeList对象,类似document.images和document.forms的属性为HTMLCollection对象 这些对象都是只读的类数组对象(7章11节),它们有length属性,也可以像正真的数组一样所有(只是读而不写),可以对一个NodeList或HTMLCollection的内容用如下标准的循环进行迭代:

            for (var i = 0; i < document.images.length; i++) //循环所有的图片
            document.images[i].style.display = "none";

不能直接在NodeList和HTML集合上调用Array的方法,但可以间接地使用:

var content Array.prototype.map.call(document.getElementsByTagName("p"),function(e){return e.innerHTML;});

HTMLCollection对象也有额外的命名属性,也可以通过数字和字符串来索引。

由于历史元素,NodeList和HTMLCollection对象也都能当做函数:以数字或字符串为参数调用它就如同使用数字或字符串索引它们一般,不鼓励这种怪异的方法。

NodeList和HTMLCollection对象也都能当做函数都不是为像javascript这样的动态语言设计的。它们都定义了item()方法,期望输入一个整数,并返回此处索引的元素。在javascript根本没有必要调用此方法,因为简单的使用数组索引就能替代。类似的,HtmlCollection定义了namedItem()方法,它返回指定属性名的值,但在javascript程序可以用数组索引或常规属性来访问。

NodeList和HTMLCollection对象不是历史文档状态的一个静态快照,而通常是事实的,并且当文档变化时它们所包含的元素能随之改变,这是其中一个重要和令人惊讶的特性。假设在一个没有<div>元素的文档中调用getElementByTagName("div"),此时返回值是一个length为0的NodeList对象。如果再在此文档中插入一个新的<div>,元素将自动成为NodeList的一个成员,并且它的length属性变成1。

通常,NodeList和HTMLCollection的实时性非常有用,如果要迭代一个NodeList对象时再文档中添加或删除元素,首先要对NodeList对象生成一个静态的副本:

        var snapshot = Array.prototype.slice.call(nodelist,0)

iiii.通过css类选取元素

HTML的class属性是用空格分开的有0个或者多个标识符的列表。class为我们描述了一种定义相关文档元素集的方法:只要元素有同样的class属性,它们就是同一个集合的一部分。class是javascript的保留词,因此客户端JavaScript使用className属性来获得HTML中class的属性。class属性通常和CSS样式表一同使用将同样的样式作用在一系列元素之上,我们将在16章继续了解它。另外,HTML5定义了一个方法,getElementsByClassName(),允许我们通过class属性的标识符选择一系列文档中的元素。

类似getElementsByTagName(),可以作用在HTML文档和HTML元素上调用getElementsByClassName() ,返回一个实时的NodeList对象,包含所有文档或元素节点中匹配的后代节点。getElementByClassName()只需要传入一个字符串参数,字符串参数可以使用空格隔开的标识符。只有当元素的class属性完全包含标识符的元素才会被匹配,但是标识符的顺序无关紧要。注意class属性和getElementsByClassName()方法都是用空格进行分隔,而不是逗号。下面是使用getElementsByClassName()的一些例子:

             // 找到所有class属性值为"warning"的元素
            var warnings = document.getElementsByClassName("warning");
             // 查找奕log命名且包含有"error"和"fatal"类的元素的所有后端
            var log = document.getElementById("log");
            var total = log.getElementsByClassName("error fatal");

如今的web浏览器会参照文档开头处声明的<!DOCTYPE>的严格程度,选择使用“怪异模式”或者“标准模式”来显示HTML文档。怪异模式存在是为了解决后向的兼容性问题,其中一个怪异模式是在class属性中和CSS样式表中的类标示符是大小写不敏感的。getElementsByClassName()使用和CSS样式表相同的匹配算法。当一个文档在怪异模式下运行时是大小写不敏感的,否则,大小写敏感。

处理IE8及其较低的版本,getElementByClassName()在所有当前浏览中都实现,IE8支持querySelectorAll()方法,下一节会介绍它。而getElementByClassName()方法则是在其之上实现的。

iiiii.通过css选择器选取元素

CSS样式有一种非常强大的语法,那就是选择器。它用来描述文档中的若干或多组元素。这里有一些最基本的例子来说明基本语法。元素可以用ID、标签名或类描述

            #nav //id="nav"的元素
            div //所有的<div>元素
            .warning //所有早class属性值包含了“waring”的元素

更一般地,元素可以给予属性值来选取

            p[lang = "fr"] //所有使用语法段落,如:<p lang="fr">
            * [name = "x"] //所有包含name = "x"属性的元素

这些基本的选择器可以组合使用:

            span.fatal.error //其class中 包含"fatal"和"error"的所有<span>元素
             span[lang = "fr"].warning //所有使用语法且class中包含"warning"的<span>元素

选择器可以指定文档结构:

        #log span //id="log"元素中所有的<span>元素
        #log>span //id="log"元素的子元素中的所有<span>元素
        body>h1:first-child //<body>的子元素中的第一个<h1>元素

选择器可以组合起来选取多个或多组元素

    div,#log //所有的元素,以及id="log"的元素

如你所见,css选择器可以让使用上述所有的方法选取元素:通过ID、名字、标签名和类名。与css3选择器的标准化一起的另一个称作“选择器API”的w3c标准定义了获取匹配一个给定选择器元素的javascript方法(选择器API标准不是HTML5的一部分,但与之有相关紧密联系,更多http://www.w3.org/TR/selectors-api/)该API的关键是Document方法querySelectorAll()。它接受包含一个css选择器的字符串参数,返回一个表示文档中匹配选择器的所有元素的NodeList对象。与前面选取的元素是不同的,querySelectorAll()返回的nodeList对象并不是实时的:它包含在调用时刻选择器所匹配的元素,但它并不更新后续文档的变化。如果没有匹配的元素,querySelectorAll()将返回一个空的NodeList对象。如果选择器字符串非法,querySelectorAll()将抛出一个异常。

除了querySelectorAll(),文档还定义了querySelector()方法。与querySelectorAll()工作原理相似,但它只返回一个匹配元素。如果没有匹配元素则返回null

css还定义了一些伪元素,":first-line"和"first-letter"等。在css中它们匹配的文本节点一部分不是实际元素。如果querySelectorAll()或querySelector()一起使用它们是不匹配的。而且很多浏览器会拒绝返回“:link”和":visited"等伪类匹配结果,因为这会泄露用户的浏览历史记录。

当期所有的浏览器都支持querySelectorAll()和querySelector()方法。但是注意,这些方法的规范并不要求支持css3浏览器:鼓励浏览器和在样式表中一样的选择器集合。当前的浏览器除IE都支持css3选择器,IE7和IE8支持css2选择器

querySelectorAll()是终级的选取元素的方法:它是一种强大的技术 ,通过让客户端javascript选择它们想要操作的元素甚至现在没有querySelectorAll()的元素支持的浏览器都可以使用css3选择器。jQuery库17章使用这种机遇css选择器的查找作为它的核心编程规范式。基于jQuery的web应用程序使用一个轻便的、跨浏览器的、和querySelectorAll()等效的方法,命名为$().

jQuery的css3选择器匹配代码以及作为一个独立的标准库并发布了,命名为Sizzle(http://sizzlejs.com/)。它已经被Dojo和其它一些客户端库所采纳.

iiiiii.document.all[]

在DOM标准化之前,IE4引入了document.all[]集合来表示所有文档中的元素(除了Text节点)。document.all[]已经成被标准的方法,现在已经废弃不使用了,但是引入它是革命性的,它在以各种方式使用的已有代码中仍然能看到

                     document.all[0] //文档中的第一个元素
                     document.all["nav"] //id或name为"nav"的元素(或多个元素)
                     document.all.nav //同上
                     document.all.tags("div") //文档中的所有div元素
                     document.all.tags("p")[0] //文档中的第一个<p>元素

3.文档结构和遍历

一旦从文档中选取了一个元素,有时需要查找文档中与之在结构上相关的部分(父亲、兄弟和子女)。文档从概念上可以看做是一颗节点对象树。节点类型定义了遍历该树所需的属性。我们将在下面的小节中介绍。另一个API允许文档作为元素的对象树来遍历,第二小节将介绍这个(通常也更容易使用的)API。

i.作为节点树的文档

Document对象、它的Element对象和文档中表示文本的Text对象都是Node对象。Node定义了以下重要属性

parentNode
该节点父节点(或者征对类似Document对象的应该是null,因为它没有父节点)
childNodes
只读数组的对象(NodeList对象),它是该节点的实时表示
firstChild、lastChild
该节点子节点的第一个和最后一个,如果该节点没有字节点则为null
nextSibling、previousSibling
该节点的兄弟节点的前一个和下一个。具有相同父节点的两个节点为兄弟节点。节点的顺序反映了它们在文档中出现的顺序。这两个属性将节点之间的以双向链表的形式连接起来。
nodeType
该节点的类型,9代表Document节点,1代表Element节点,3代表Text节点,8代表Comment节点,11代表DocumentFragment节点

nodeValue
Text节点或Comment节点的文本内容
nodeName
元素的标签名,以大写形式表示

使用这些Node属性,可以用下面类似的表达式得到文档的第一个节点下面的第二个子节点的引用

            document.childNodes[0].childNodes[1];
            document.firstChild.firstChild.nextSibling;

假如上述的代码如下:

        <html>
            <head>
            <title>test</title>
            </head>
            <body>
                hello world!
            </body>
        </html>

那么第一个字节点下的第二个元素就是BODY,它的nodeType为1,nodeName为BODY

但请注意,该API对文档文本的变化极其敏感。例如,如果修改了文档,在<html>和<head>标签插入一个新,那么表示该新航的Text节点就是文档的第一个子节点下面的第一个子节点,并且<head>元素就是第二个,子节点不是body元素了。

ii.作为元素树的文档

当将主要的兴趣点集中在文档中的元素上而并非它们之间的文本(和它们之间的空白),我们可以使用另外一个更有用的API.它将文档看做是Element对象树,忽略部分文档:Text和Comment节点。

该API的第一部分是Element对象的children属性。类似ChildNodes,它是一个NodeList对象,但不同是的children列表只包含Element对象。Children非标准属性,但是在所有当前浏览器里都能工作。IE也实现好一段时间了,大多数浏览器也如法炮制,最后采纳的是Firefox3.5。

注意:Text和Comment节点没有children属性,它意味着上述Node.parentNode属性不可能返回Text或Comment节点。任何Element的parentNode总是令一个Element,或者追溯到树根的Document或DocumentFragment节点

基于元素的文档遍历API的第二部分是Element属性,后者类似Node对象的子属性和兄弟属性:

firstElementChild,lastElementChild
类似firstChild和lastChild,但只代表Element

nextElementSibling,previousElementSibing
类似nextSibing和previousSibling,但只代表兄弟Element

childElementCount
子元素的数量。返回的值和chilren.length值相等

子元素和兄弟元素的属性是标准属性,并在除IE之外的浏览器已经实现http://www.w3.org/TR/ElementTraversal/

由于逐个元素的文档遍历的API并未完全标准化,我们任然可以通过像下面的例子可移植的遍历函数那样实现这种功能:

             /*****可移植的遍历函数******/
            
            /**
             * 返回元素e的第n层祖先元素,如果不存在此类祖先或祖先不是Element,例如(Document或者DocumentFragment)则返回null
             * 如果n为0,则返回e本身,如果n为1(或省略),则返回父元素。如果n为2,则返回祖父元素,依次类推
             **/
            function parent(e, n) {
                    if (n === undefined) n = 1;
                    while (n-- && e) e = e.parentNode;
                    if (!e || e.nodeType !== 1) return null;
                    return e;
                }
            
            
                /**
                 *返回元素e的第n个兄弟元素,如果n为正,返回后续的第n个兄弟元素;
                 * 如果n为负,返回前面n个兄弟元素,如果n为零,返回e本身
                 **/
            function sibling(e, n) {
                    while (e && n !== 0) { //如果e未定义,立刻返回它
                        if (n > 0) { //查找后续的兄弟元素
                            if (e.nextElementSibling) e = e.nextElementSibling;
                            else {
                                for (e = e.nextSibling; e && e.nodeType !== 1; e = e.nextSibling)
                                /*空循环*/
                                ;
                            }
                            n--;
                        } else { //查找前边的兄弟元素
                            if (e.previousElementSibling) e = e.previousElementSibling;
                            else {
                                for (e = e.previousElementSibling; e && e.nodeType !== 1; e = e.previousElementSibling)
                                /*空循环*/
                                ;
                            }
                            n++
                        }
                    }
                    return e;
                }
            
            
                /**
                 * 返回元素e的第n代子元素,如果不存在则为null
                 * 负值n代表从后往前计数,0表示第一个子元素,而-1代表最后一个,-2代表倒数第二,依次类推
                 **/
            function child(e, n) {
                if (e.children) { //如果children数组存在
                    if (n < 0) n += e.children.length; //转换负的n为数组索引
                    if (n < 0) return null; //如果它仍为负,说明没有子元素
                    return e.children[n]; //返回值指定的子元素
                }
                //如果e没有children数组,找到第一个字元素并向前数,或找到最后一个子元素并往回鼠
                if (n >= 0) { //非负,从第一个元素向前数
                    //找到e的第一个子元素
                    if (e.firstElementChild) e = e.firstElementChild;
                    else {
                        for (e = e.firstElementChild; e && e.nodeType !== 1; e = e.nextSibling)
                        /*空循环*/
                        ;
                    }
                    return sibling(e, n); //返回第一个子元素的第n个兄弟严肃
                } else { //n为负数,从第一个元素往回数
                    if (e.lastElementChild) e = e.lastElementChild;
                    else {
                        for (e.lastElementChild; e && e.nodeType !== 1; e = e.previousElementSibling)
                        /*空循环*/
                        ;
                    }
                    return sibling(e, n + 1); //+1来转化最后1个子元素的最后1个兄弟元素
                }
            }

自定义Element的方法

所有当前浏览器,都实现了DOM,故类似Element和HTMLDocument(IE8支持Element、HTMLDocument和Text的可扩展属性,但不支持Node,Document、HTMLDocument或HTMLElement更具体的子类型的可扩展属性)等类型都是想String和Array都是类。它们不是构造函数(将在本章后面看到如何创建新的Element对象),但它们有原型,可以用自定义方法扩展它

            Element.prototype.next = function() {
                if (this.nextElementSibling) return this.nextElementSibling;
                var sib = this.nextSibling;
                while (sib && sib.nodeType !== 1) sib = sib.nextSibling;
                return sib;
            };

上文中的可移植的遍历函数兵没有定义为Element方法是因为这种技术在IE7中不支持

尽管如此,如果希望将IE专有的特性在除了IE之外的其它浏览器中得以实现,这种扩展DOM类型的能力是非常有用的。从上面注意到Element非标准children属性由IE首先引入,并已经被其它浏览器所采纳。类似Firefox3.0不支持它的浏览器可以用下面的代码模拟此属性

             //在不包含此属性的IE浏览器模拟Element.children属性
             //注意:返回值为静态数组,而不是时实的NodeList对象
            if (!document.documentElement.children) {
                Element.prototype.__defineGetter__("children", function() {
                    var kids = [];
                    for (var c = this.firstChild; c != null; c = c.nextSibling)
                        if (c.nodeType === 1) kids.push(c);
                    return kids;
                });
            }

__defineGetter__方法,(6章7节i有介绍)完全是非标准的,但它用来一直类似代码非常完美。

4.属性

HTML元素包含一个标签和一组称为属性(Attribute)的名/值对组成。例如:<a>元素定义了一个超链接,通过使用href属性来指向链接的地址。这些元素的属性值代表在这些元素HtmlElement对象的属性(property)是可用的。DOM同样定义了一些API来设置和获取XML属性和非标准化的HTML属性。下面将详细介绍这些特性。

i.HTML属性作为Element属性

表示HTML文档中元素的HTMLElement对象定义了读/写对的属性HTMLElement定义了通用的HTTP属性(id,title ,lang,dir)和像onclick这样的事件处理属性,特定Element的子类型定义了这些元素的特定属性。为了查询一个图片的URL地址,你可以使用表示<img>元素的HTMLElement的src属性:

            var image = document.getElementById("myimage");
            var imgurl = image.src;//src是图片的url
            image.id === "myimage"; //true

相似地,你通过下面这样的代码设置<form>元素提交的属性:

        var f = document.forms[0]; // First <form> in the document
        f.action = "http://www.example.com/submit.php"; // Set URL to submit it to.
        f.method = "POST";

HTML属性名是大小写不敏感的,但是JavaScript的属性名是大小写敏感的。将一个属性名转化为JavaScript属应该采用小写。但是如果一个属性名包含不止一个单词的长度,以驼峰方式命名:如defaultChecked和tabIndex。

有些HTML的属性的值通常是字符串。当属性为布尔值或数值(例如,<input>元素的defalultChecked和maxLength属性,)属性值是布尔值或数字,而不是字符串。事件处理程序属性值总是Function对象(或null)。

HTML5规范定义了一个新的属性(如<input>和相关元素的form属性)用以ID转换为实际的Element对象。最后,任何元素的style属性值是CSSStyleDeclaration对象,而不是字符串,我们将在14章看到这个重要的属性值的更多信息

这个基于属性的API用来获取和设置属性,但没有定义任何从元素删除属性的方法。奇怪的是,delete操作符也无法完成此项目。下一节描述可以实现此目的方法。

ii.取得和设置非标准的HTML属性

像上面描述的那样,HTMLElement及其子类型定义一个了标准的HTML的属性。Element类型同样定义了getAttribute()和setAttribute()方法可以查询和设置这些非准的HTML属性,当然也可以用来查询和设置XML文档中元素上的属性。

        var image = document.images[0];
        var width = parseInt(image.getAttribute("width"));
        image.setAttribute("class","firstImage");

从上面看出基于属性的API方法两个很重要的区别。首先,属性值都被视作字符串。getAttribute()不会返回数值、布尔值或者对象。其次,这些方法是用标准的属性名,简基于属性的API,甚至这些明川为javascript保留字时都不例外。对于HTML元素来说,属性名名不区分大小写。

Element还定义了两个相关方法,hasAttribute()和removeAttribute(),这个方法会检查命名属性是否存在和完全地删除这个属性。属性为布尔值特别有用:有些属性(如HTML的表单元素的disabled属性)在一个元素中是否存在是重点关键,而其他值无关紧要。

如果你使用了包含了另外一个命名空间的属性名的XML文档,你可以使用四种变种方法:getAttributeNS(),setAttributeNS,hasAttributeNS()和removeAttributeNS()。这些方法需要两个参数。第二个参数通常是属性的有效名字,setAttributeNS(),第二个参数是必须要求的,包含了命名空间的前缀。可以在本书的第四部分中阅读更多关于命名空间的属性的方法。

ii.数据集属性

有时在HTML元素上绑定上额外的信息是很有用的,特别是当JavaScript代码需要选择并按一定方法操纵他们的时候。有时可以通过为class属性添加特定的标识符来完成一些工作。另外一些时候,对于某些复杂的数据而言,客户端程序员需要非标准的方法实现。如上文提到的,你可以使用getAttribute()和setAttribute()方法来读取和改写非标准属性的值。当然你所付出的代价是HTML或许不是合法有效的。

HTML5提供了一个解决方法,任意以小写data-作为前缀的属性名字都是合法的。这些“数据集属性”不会影响元素的表现,它们定义了标准的,附加额外数据的方法,并不是在文档合法性上做出让步。

HTML5也在元素对象上定义了dataset属性。这个属性引用一个对象,它的各个属性对于去掉前缀data-属性。因此dataset.x应该保持data-x属性的值。连带字符串属性应该书序驼峰命名规则:data-jquery-test属性变成dataset.jqueryTest属性。

一个具体的例子,看如下标记

        <span class="sparkline" data-ymin="0" data-ymax="10">
         1 1 1 2 2 3 4 5 5 4 3 5 6 7 7 4 2 1
        </span>

火花线是一个图像——通常是一条线——用以表现连续的文字。为了创建一个条火花线,也许可以如下代码提前上述的dataset属性的值 , 你需要取得 dataset 属性中的值。

             //假设ES5 Array.map() 方法 (或类似的方法)有定义
            var sparklines = document.getElementsByClassName("sparkline");
            var dataset = sparklines[i].dataset;
            var ymin = parseFloat(dataset.ymin);
            var ymax = parseFloat(dataset.ymax);
            var data = sparklines[i].textContent.split(" ").map(parseFloat);
            for (var i = 0; i < sparklines.length; i++) {
                drawSparkline(sparklines[i], ymin, ymax, data); // 该方法未实现
            }

dataset属性有些浏览器还未实现,可以这样写

            var sparklines = document.getElementsByClassName("sparkline");
            for (var i = 0; i < sparklines.length; i++) {
                var elt = sparklines[i];
                var ymin = parseFloat(elt.getAttribute("data-ymin"));
                var ymin = parseFloat(elt.getAttribute("data-ymax"));
                var points = elt.getAttribute("data-points");
                var data = elt.textContent.split(" ").map(parseFloat);
                drawSparkline(elt, ymin, ymax, data); // 此方法未实现
            }

注意:dataset属性是一个(实现的时候会这样)元素的data-属性的实时、双向接口。设置或或删除dataset的一个属性就等同于设置或移除对于元素的data-属性
上面例子中的drawSparkline()函数是虚构的,19章会给出<canvas>绘制类似火花线的标记代码。

iiii.作为Attr节点的属性

还有一种是以Element的属性的方法。Node类型定义了attributes属性。征对非Element对象的任何节点,该属性为null。对于Element对象,attributes属性是只读的类数组对象,它代表元素的所有属性。类似NodeLists,attributes对象是实时的。它可以用数字索引访问,这意味着可枚举元素的所有属性。并且,它也可以用属性名索引:

            document.body.attributes[0]; //<body>元素的第一个属性
            document.body.attributes.bgColor //<body>元素的bgColor属性
            document.body.attributes["ONLOAD"] //<body>元素的onload属性

当索引attributes对象时得到的值是Attr对象。Attr对象的一类特殊的Node,但从来不会像Node一样去用。Attr的name和value属性返回属性的名字和值。

 5.元素的内容

看上文的列子,并问自己一个问题<p>元素的“内容”是什么?回答这个问题有三个方法:

  • 内容是HTML字符串"This is a <i>simle</i> document"
  • 内容是纯文本字符串“This is a simple document”.
  • 内容是一个Text节点、一包含了一个Text子节点的Element节点和另外一个Text节点

每一种回到都有效,后面几节我们将解释如何使用HTML表示、纯文本和元素内容树状表示

i.作为HTML的元素内容

读取Element的innerHTML属性作为字符串标记返回那个元素的内容。在元素上设置该属性调用了web浏览器解释器,用新的字符串内容解析展现形式替换元素当前内容。(不要管它的名字,除了在HTML元素上,innerHTML也可以在XML元素上使用)

web浏览器很擅长解析HTML,通常设置innerHTML效率非常高,甚至在指定的值需要解析时效率也相当不错,注意对innerHTML属性用“+=”操作符重复追加一小段文本的效率非常低下,因为它即要序列化又要解析

innerHTML是在ie4引入的。但随着html5到来它才变得标准化。HTML5说innerHTML应该在Document节点以及Element节点上工作正常,但这还未被普遍的支持。

HTML5还标准化了outerHTML属性当查询outerHTML时,返回HTML或XML标记字符串包含被查询元素的开头和结尾标签。当设置元素的outerHTML时,元素本身被新内容所替换。只有Element节点定义了outerHTML属性,Document节点则无。目前测试,outerHTML在所有的浏览器中都支持。(见本章后面的例子:基于innerHTML实现outerHTML

IE引入的另外一个特性是insertAdjacentHTML()方法,在HTML5标准化它将任意HTML标记字符串插入到指定的元素“相邻”的位置标记是该方法的第二个参数。并且相邻的精确含义依赖于第一个参数的值,第一个参数为具有以下值之一的字符串:"beforebegin"、"afterbegin"、"beforeend"、afterend、.这些值对于如下

 

 

insertAdjacentHTML()插入点

(本章后面展示了用innerHTML属性实现的insertAdjacentHTML(),也展示了如何写出一个不要字符串参数来指定插入点的HTML插入方法)

ii.作为纯文本的元素内容

有时候需要查询纯文本形式的元素内容,或者在文档中插入纯文本(不必转义HTML标记中使用的尖括号和&符号)。标准的方法是用NodetextContent属性来实现的。

        var para = document.getElementsByTagName("p")[0];
        var text = para.textContent; //'this is a simple document'
        para.textContent = "hello world!" //修改段落内容

textContent属性在除了IE的所有当前浏览器都支持。在IE中,可以用Element的innerText属性来代替。围绕IE4引入了innerText属性,它除了foxfire所有的浏览器都支持

textContent和innerText属性非常相似,通常可以互相替换使用。不过要小心空元素(javascript中的字符串""是假值)和未定义属性之间的区别。

            /**
             *一个参数,返回元素的textContent或innerText
             * 两个参数,用value值代替参数设置元素的textContent或innerText
             **/
            function textContent(element, value) {
                var content = element.textContent; //检测textContent是否定义
                if (value === undefined) { //没传递value,因此返回当前文本
                    if (content !== undefined) return content;
                    else return element.innerText;
                } else { //传递value,设置文本
                    if (content !== undefined) element.textContent = value;
                    else element.innerText = value;
                }
            }

textContent属性就是将制定的元素的后代Text节点简单的串联在一起innerText没有一个明确的指定行为,但innerText不返回<script>元素的内容。它忽略多于空白,并试图保留表格格式。同时,innerText征对某些表格元素(<table>,<tobody>,<tr>)是只读的属性

<script>中的元素文本

<script>元素中的文本内联的<script>元素(也就是没有src属性的那一种)有一个text属性来获取它们的文本。<script>元素的内容不会在浏览器中显示,HTML解析器会忽略方括号和脚本中的&符号。这样使得<script>元素成为某些应用程序用来嵌入任意文本内容的一个理想的地方。简单地设置元素的type属性的(如“text/x-custom-data”)说明了这部分这部分脚本是不可制定的JavaScript代码。如果你这样做了,JavaScript解释器会忽略这些脚本,但是text属性仍然会在现有文档中返回这部分数据给你

 

iii.作为Text节点的元素内容

另一种方法出路元素的内容是当做一个子节点列表,每个子节点都可能有它子节点一组子节点,当考虑元素的内容时,通常感兴趣的就是Text节点。在XML文档中,你必须处理好CDATSection节点,它是Text的子类型,代表CDATA短段的内容。

下面的例子展示了一个textContent()函数,它递归地遍历元素的子节点,然后连接后代节点中所有的Text节点文本,为了理解代码,必须回想一下nodeValue属性(定义在Node类型中),它保存Text节点的内容。

             /**查找元素的后代节点中所有的Text节点**/
             //返回元素e的纯文本内容,递归进入其子元素
             //该方法的效果类似于textContent属性
            function textContent(e) {
                var child, type, s = ""; //s保存所有字节点文本
                for (child = e.firstChild; child != null; child = child.nextSibling) {
                    type = child.nodeType;
                    if (type === 3 || type === 4) //text和CDATASection节点
                        s += child.nodeValue;
                    else if (type === 1)
                        s += textContent(child);
                }
                return s;
            }

nodeValue属性可以读/写,设置它可以改变Text或CDATASection节点所显示的内容。Text和CDATA都是CharacterData子类型,可以在第四部分查看相关信息。CharacterData定义了打他属性,它和nodeValue的文本相同,以下函数通过设置data属性将Text节点的内容转换为大写。

             //递归把n的后代子节点中的所有Text节点内容转换为大写形式
            cd = document.getElementsByTagName("p")[0];

            function upCase(n) {
                if (n.nodeType == 3 || n.nodeType == 4) //如果n是Text或CDATA节点
                    n.data = n.data.toUpperCase(); //转换为大写
                else
                    for (var i = 0; i < n.childNodes.length; i++)
                        upCase(n.childNodes[i]);
            }

 6.创建、插入、和删除节点

我们已经看到用HTML和纯文本字符串如何来查询和修改文档内容,也已经看到我们能够遍历Document来检查组成Document的每个Element和Text节点,在每个节点基本修改文档也是有可能的。Document类型定义了创建Element和Text对象的方法,Node类定义了在节点树中插入、删除和替换的方法。

这里有个小例子:

            function loadsync(url){
                var hand = document.getElementsByTagName("head")[0];
                var s = document.createElement("script");
                s.src = url;
                hand.appendChild(s);
            }

以下小节包含了节点创建、插入和删除的更多细节和具体例子。也包含在操作多个节点时的一种捷径,使用DocumentFragment

i.创建节点

如上代码所示,创建新的Element节点可以使用Document对象的createElement()方法。给方法传递元素的标签名:对HTML文档来说名字不容易区分大小写,对XML文档则区分大小写。

Text节点用类似的方法创建

            var newnode = document.createTextNode("Text node");
            var newelement = document.createElement("p");

Document也定义了一些其他的工厂方法,如不经常使用的createComment(),6节iiii小节使用了createDocumentFragment()方法。在使用了XML命名空间的文档中,可以使用createElementNS()来同时指定命名空间和URI和待黄建的Element标签的名字

另一种创建新文档的方法是复制已经存在的节点。每个节点上有一个cloneNode()方法来返回该节点的一个全新副本。给参数传递true也能够递归的赋值所有后代及节点,或传参数false只是执行一个浅复制。在除了IE的其它浏览器中,Document对象还定义了一个类似的方法胶importNode().如果给它传递另一个稳定的一个节点,它将返回一个适合本文档插入的节点的副本。传递true为第二参数,此方法将递归导入所有的后代节点。

ii.插入节点

一旦有了有了一个新的节点,就可以用Node的方法appendChild()或insertBefore()将它插入到文档中.appendChild()是需要插入的Element节点上调用的,它插入指定的节点将成文那个节点的最后一个节点。

insertBefore()就像appendChild()一样,接受两个参数,第一个是待插入的节点,第二个参数是已经存在的节点,新节点将插入该节点的前边。该方法应该是在新节点的父节点上调用,第二个参数必须是该父节点的子节点。如果传递null作为第二个参数,insertBefore()的行为类似appendChild(),它将节点插入在最后。

这是一个在数字索引的位置插入节点的简单函数。同时展示了appendChild()和insertBefore()方法

             //将child节点插入到parent中,使其成为第n个子节点
            function inserAt(parent, child, n)
            if (n < 0 || n > parent.childNodes.length) throw new Error("invalid index");
            else if (n == parent.childNodes.length) parent.appendChild(child);
            else parent.insertBefore(child, parent.childNodes[0]);

如果调用appendChild或insertBefore()将已存在的文档中的一个节点再次插入。那个节点将自动从它当前的位置删除并在新的位置重新插入:没必要显式删除改节点。

下面的的例子展示了一个函数,基于表格指定列中单元格的值来进行排序。它没有创建任何新的节点。只是用appendChild()改变已存在的节点。

             /**表格的排序**/
             //根据指定表格每行第n个单元格的值,对第一个<tbody>中的进行排序
             //如果存在comparator函数则使用它,否则按字母表顺序比较
            function sortrows(table, n, comparator) {
                    var tbody = table.tBodies[0]; //第一个<tbody>,可能是隐式窗口的
                    var rows = tbody.getElementsByTagName("tr"); //tbody中所有行
                    rows = Array.prototype.slice.call(rows, 0); //真实的数组
                    //基于第n个<td>元素的值对行排序
                    rows.sort(function(row1, row2) {
                        var cell1 = row1.getElementsByTagName("td")[n]; //获得第n个单元格
                        var cell2 = row2.getElementsByTagName("td")[n]; //两行都是
                        var val1 = cell1.textContent || cell1.innerText; //获得文本内容
                        var val2 = cell2.textContent || cell2.innerText; //同上,两单格都是
                        if (comparator) return comparator(val1, val2); //    进行比较
                        if (val1 < val2) return -1;
                        else if (val1 > val2) return 1;
                        else return 0;
                    });
                    //在tobody中按他们的顺序把行添加到最后
                    //这将自动把它们从当前位置移走,故没必要预先删除它们
                    //如果<tbody>还包含除了<tr>的任何其他元素,这些节点都将会悬浮到顶部位置
                    for (var i = 0; i < rows.length; i++) tbody.appendChild(rows[i]);
                }
                //查找表格的<th>元素,假设只有一行,它们可以单击
                //以便单击列标题,按列对行排序。

            function makeSortable(table) {
                var headers = table.getElementsByTagName("th");
                for (var i = 0; i < headers.length; i++) {
                    (function(n) { //嵌套函数来创建本地域
                        headers[i].onclick = function() {
                            sortrows(table, n);
                        };
                    }(i)); //将i的全局变量赋值给局部变量n
                }
            }

iii.删除和替换节点

removeChild()就是从文档树中删除一个节点,但是请小心:该方法不是在待删除的节点上调用,而是(就像其名字的一部分“child”所暗示的一样)在其父节点上调用。在父节点上调用该方法,并将需要删除子节点作为方法参数传递给它。在文档中删除n节点。代码可以这样写:

        n.parentNode.removeChild(n);

replaceChild()方法删除一个子节点并用一个新的节点取而代之。在父节点上调用该方法,第一个参数是新节点第二个参数是要替代的节点,例如用一个文本字符串来代替节点n,可以这样写:

        n.parentNode.replaceChild(document.createTextNode("[redactd]"),n);

以下的函数展示了replaceChild()的另一种用法

            function embolden(n) {
                //假设参数为字符串而不是节点,将其当做元素的id
                if (typeof n == "string") n = document.getElementById(n);
                var parent = n.parentNode; // 获得n的父节点
                var b = document.createElement("b"); //创建一个b元素
                parent.replaceChild(b, n); //使用<b>元素替换节点n
                b.appendChild(n); //使你成为<b>元素的子节点
            }

本章5.i介绍过元素的outerHTML属性,也解释了一些版本的firefox还未实现它。下面的例子将展示在firefox(和其它任何支持innerHTML的浏览器,要有一个可扩展的Element.prototype对象,还有一些方法来定义属性的getter和setter)如何来实现该属性。同时,代码还展示了removeChild()和cloneNode()的实际用法。

             /**使用innerHTML实现outerHTML属性**/
             //为那些不支持它的浏览器实现outerHTML属性
             //假设浏览器的确支持innerHTML,并有个可扩展的Element.prototype
             //并且可以定义getter和setter
            (function() {
                    //如果outer存在,直接返回
                    if (document.createElement("div").outerHTML) return;
                    //返回this所引用元素的外部HTML
                    function outerHTMLGetter() {
                            var container = document.createElement("div"); //虚拟元素
                            container.appendChild(this.cloneNode(true)); //复制到虚拟节点
                            return container.innerHTML; //返回虚拟节点的innerHTML
                        }
                        //用指定的值设置元素的外部的HTML

                    function outerHTMLSetter(value) {
                            //创建一个虚拟元素,设置其内容为指定的值
                            var container = document.createElement("div");
                            container.innerHTML = value;
                            //将虚拟元素的节点全部移动到文档中
                            while (container.firstChild) //循环知道container没有子节点为止
                                this.parentNode.insertBefore(container.firstChild, this);
                            //删除被取代的节点
                            this.parentNode.removeChild(this);
                        }
                        //现在使用着两个函数作为所有Element对象的outerHTML属性的getter和setter
                        //如果它存在则使用ECMAScript5的Object.defineProperty()方法
                        //否则,退而求其次,使用__defineProperty__()和__definedSetter__()
                    if (Object.defineProperty) {
                        Object.defineProperty(Element.prototype, "outerHTML", {
                            get: outerHTMLGetter,
                            set: outerHTMLSetter,
                            enumerable: false,
                            configurable: true
                        });
                    } else {
                        Element.prototype.__defineGetter__("outerHTML", outerHTMLGetter);
                        Element.prototype.__defineSetter__("outerHTML", outerHTMLSetter);
                    }
                }
                ());

iiii.使用DocumentFragment

DocumentFragment是一种特殊的Node,它为其它节点创建一个临时的容器。像这样创建一个DocumentFragment:

var frag = document.createDocumentFragment();

像Document节点一样,DocumentFragment是独立的,而不是任何其它文档的一部分。它的parentNode总为null但类似Element,它可以有任意多的子节点,可以用appendChild(),insertBefore()等方法来操作他们

DocumentFragment的特殊之处在于它是的一组节点被当做一个节点来看待:如果给appendChild()、insertBefore()或replaceChild()传递一个DocumentFragment,其实是将所有的子及诶单插入到文档中,而非片段本身。(文档片段的子节点从片段移动到文档中,文档片段清空以便重用)。下面的例子是函数使用DocumentFragment来倒序排列的一个节点的子节点。

             //倒序排列节点n的子节点
            function reverseDome(n) {
                //创建一个DocumentFragment()
                var f = document.createDocumentFragment();
                //从后至前循环子节点,将每一个字节点移动到文档片段中
                //n的最后一个子节点变成第一个子节点
                //注意,给f添加一个节点,该节点自动会从n中删除
                while (n.lastChild) f.appendChild(n.lastChild);
                
                //最后将所有的子节点一次移动回n中
                n.appendChild(f);
            }

 

下面的例子使用innerHTML属性和DocumentFragment实现insertAdjacentHTML()方法(5.i节)。还定义了一些名字更符合逻辑的HTML插入函数,可以替换让人迷惑的insertAdjacentHTML()API内部工具函数fragment()代码中最有用的部分:它反映对一个指定的HTML字符串文本解析后的DocumentFragment

             /**用innerHTML实现insertAdjacentHTML**/
             //本模块为不支持它的浏览器定义了Element.insertAdjacentHTML
             //还定义了一些可移植的HTML插入函数,它们的名字比insertAdjacentHTML更符合逻辑
             //Insert.before、Insert.after、Insert.atStart和Insert.atEnd
            var Insert = (function() {
                //如果命名空间有原生的insertAdjacentHTML.在4个函数名更名了的HTML使用它。
                if (document.createElement("div").insertAdjacentHTML) {
                    return {
                        before:function(e, h) {e.insertAdjacentHTML("beforebegin", h);},
                        after:function(e, h) {e.insertAdjacentHTML("afterend", h);},
                        atStart:function(e, h) {e.insertAdjacentHTML("afterbegin", h);},
                        atEnd:function(e, h) {e.insertAdjacentHTML("beforeend");}
                    };
                }
                //否则,无元素的insertAdjacentHTML同样实现4个插入函数,并定义insertAdjacentHTML
                //首先,定义一个工具函数,传入HTML字符串,返回一个DocumentFragment
                //包含解析后的HTML的表示
                function fragment(html) {
                    var elt = document.createElement("div"); //创建空元素
                    var frag = document.createDocumentFragment(); //创建文本片段
                    elt.innerHTML = html; //设置元素内容
                    while (elt.firstChild) //移动所有的节点
                        frag.appendChild(elt.firstChild); //从elt到frag
                    return frag; //返回frag
                }
                var Insert = {
                    before:function(elt, html) {elt.parentNode.insertBefore(fragment(html), elt);},
                    after:function(elt, html) {elt.parentNode.insertBefore(fragment(html), elt.nextSibling);},
                    atStart: function(elt, html) {elt.insertBefore(fragment(html), elt.firstChild);},
                    atEnd: function(elt, html) {elt.appendChild(fragment(html));}
                    };
                    //基于以上函数实现insertAdacentHTMLs
                    Element.prototype.insertAdjacentHTML = function(pos, html) {
                        switch(pos.toLowerCase()) {
                            case "beforebegin":return Insert.before(this.html);
                            case "afterend":return Insert.after(this.html);
                            case "afterbegin":return Insert.atStart(this, html);
                            case "beforeend":return Isert.atEnd(this, html);
                        }        
                    };
                        return Insert; //最后返回4个插入函数
            }());

 7.例子:生成目录表

本例子说明了如何为文档动态的创建一个目录表。它展示了上一节所描述的文档校本化的很多概念、元素选取、文档遍历、元素属性设置、innerHTML属性设置和在文档中创建于插入新的节点等。本例子的注释也比较详尽。

        <style type="text/css">
            #TOC {
                border: 1px solid #000;
                margin: 10px;
                padding: 10px;
            }
            .TOCEntry {
                font-family: sans-serif;
            }
            .TOCEntry a {
                text-decoration: none;
            }
            .TOCLevel1 {
                font-size: 16pt;
                font-weight: bold;
            }
            .TOCLevel2 {
                font-size: 12px;
                margin-left: .5in;
            }
            .TOCSectNum:after {
                content: ":";
            }
            .TOCsectNum {
                display: none;
            }
        </style>
             /**一个动态自动生成的目录表**/
            /**
             * 这个 模块注册一个可在页面加载完成后自动运行的匿名函数。当执行这个函数时会去文档中查找id为“TOC”的元素
             *
             * 生成的TOC目录应该具有自己的css样式。这个目录区域的样式className设置为“TOCEntry”
             * 统一我们为不同层级的目录模板标题定义不同的样式。<h1>标签生成的标题className为“TOCLevel1”,<h2>标签生成的标题className为"TOCLevel2",依次类推
             *段编号的样式为“TOCSectNum”
             * 这个模块需要onLoad()工具函数
             **/
                (function() { //匿名函数定义了一个局部作用域
                //查找TOC容器元素
                //如果不存在,则在文档开头处新建一个
                var toc = document.getElementById("TOC");
                if (!toc) {
                    toc = document.createElement("div");
                    toc.id = "TOC"
                    document.body.insertBefore(toc, document.body.firstChild);
                }
                //查找所有的标题元素
                var headings;
                if (document.querySelectorAll) //我们能否使用这个简单的方法?
                    headings = document.querySelectorAll("h1,h2,h3,h4,h5,h6");
                else //否则查找的方法稍微复杂些
                    headings = findHeadings(document.body, []);
                //递归遍历document的body,查找元素
                function findHeadings(root, sects) {
                        for (var c = root.firstChild; c != null; c = c.nextSibling) {
                            if (c.nodeType !== 1) continue;
                            if (c.tagName.length == 2 && c.tagName.charAt(0) == "H")
                                sects.push(c);
                            else
                                findHeadings(c, sects);
                        }
                        return sects;
                    }
                    //初始化一个数组来保持跟踪章节好
                var sectionNumbers = [0, 0, 0, 0, 0, 0];
                //现在循环已经找到的标题元素
                for (var h = 0; h < headings.length; h++) {
                    var heading = headings[h];
                    //跳过在TOC容器中的标题元素
                    if (heading.parentNode == toc) continue;
                    //判定标题的级别
                    var level = parseInt(heading.tagName.charAt(1));
                    if (isNaN(level) || level < 1 || level > 6) continue;
                    
                    //对于重要的标题级别增加sectionNumber对于的数字
                    //重置所有标题比它级别低的数字为零
                    sectionNumbers[level-1]++;
                    for(var i = level; i<6;i++) sectionNumbers[i] =0;
                    
                    //现在讲所有标题级别的章节号组合产生一个章节号,如2.3.1
                    var sectionNumber = sectionNumbers.slice(0,level).join(".")
                    
                    //为标题级别增加章节号
                    //把数字放在<span>中,是的其可以用样式修饰
                    var span = document.createElement("span");
                    span.calssName = "TOCSectNum";
                    span.innerHTML =sectionNumber;
                    heading.insertBefore(span,heading.firstChild);
                    
                    //用命名的锚点将标题包起来,以便它增加链接
                    var anchor = document.createElement("a");
                    anchor.name = "TOC"+ sectionNumber;
                    heading.parentNode.insertBefore(anchor,heading);
                    anchor.appendChild(heading);
                    
                    //现在为该节点创建一个链接
                    var link = document.createElement("a");
                    link.href = "#TOC" + sectionNumber;//链接目标的地址
                    link.innerHTML = heading.innerHTML;//链接文本与实际标题一致
                    
                    //将链接放在一个div中,div用于基于级别名字的样式修饰
                    var entry = document.createElement("div");
                    entry.className = "TOCEntry TOCLevel" +level;
                    
                    entry.appendChild(link);
                    //改div添加到TOC容器
                    toc.appendChild(entry)
                    
                }
            }); 

 

8.文档和元素的几何形状和滚动

在本章中,到目前为止我们考虑的文档被看做是元素和文本节点的抽象树。但是当浏览器在在窗口中渲染文档时,它创建文档的一个视觉表现层,在那个每个元素有自己的位置和尺寸。通常web应用程序可以将文档看做是元素的树,并且不用关心在屏幕上是如何渲染的。但有时候。判断一个元素精确的几个形状也是非常有必要的。

例如:在14章我们看到利用css元素指定位置。如果想要css动态定文一个元素(如工具的提示或插图)到某个已经由浏览器定文后的普通元素的旁边,首先要判断那个元素的当前位置。

本节阐述了在浏览器窗口中完成文档的布局后,怎么才能在抽象的基于树的文档模型与几何形状的基于坐标的视图之间来回变换。本节描述的属性和方法以及在浏览器中实现有很长一段时间了,有些是IE特有的,有些到IE9才实现。大家可以参考W3C的标准化流程,作为CSSOM-View模块www.w3.org/TR/cssom-view/

i.文档坐标和视口坐标

视口坐标是值吓死文档内容浏览器的部分,不包括浏览器的外壳。
无论在何种情况下讨论元素的位置,必须清楚所使用的坐标是文档坐标还是视口坐标。

/*查询窗口滚动条的位置*/
//以一个对象的x和y属性的方法返回滚动条的偏移量
function getScrollOffsets(w) {
    //使用指定才窗口,如果不带参数则使用当前窗口
    w = w || window;

    //除了IE8及更早的版本以外,其它的浏览器都能用
    if (w.pageXOffset != null) return {x: w.pageXOffset, y:w.pageYOffset};

    //对标准模式下的IE,或任何浏览器
    var d = w.document;
    if (document.compatMode == "CSS1Compat")
        return {x:d.documentElement.scrollLeft, y:d.documentElement.scrollTop};

    //怪异模式下的浏览器
    return { x: d.body.scrollLeft, y: d.body.scrollTop };
}

有的时候判断视口的尺寸也是非常有用的,例如:为了确定文档的那些不法是当前可见的,利用滚动偏移量查询视口的简单方法在IE8及更早的版本中无法工作。而且该技术在IE中的运行方式还要基于浏览器的模式下面的例子便捷的查询视口尺寸,注意,它和上面的代码十分相似。

             /*查询窗口的视口尺寸*/
             //作为一个对象的w和h属性返回视口的尺寸
            function getViewportSize(w) {
                //使用指定的窗口,如果不带参数则使用当前窗口
                w = w || window;
                //除了ie8和更早的版本,其它浏览器都能用
                if (w.innerWidth != null) return {
                    w: w.innerWidth,
                    h: w.innerHeight
                };
                //对于标准模式下的IE或其任何浏览器
                var d = w.document;
                if (document.compatMode == "CSS1Compat")
                    return {
                        w: d.documentElement.clientWidth,
                        h: d.documentElement.clientHeight
                    };
                //对于怪异模式下的浏览器
                return{w:d.body.clientWidth,h:d.body.clientHeight};
            }

ii.查询元素的几何尺寸

判定一个元素尺寸和位置最简单的方法是调用它的getBoundingClientRect()方法。这个方法是在IE5中引入的。而现在所有的浏览器都实现 了,它不需要参数,返回一个有left,right,top,bottom的属性对象。left和top表示左上角的x和y坐标right和bottom属性表示元素右下角的x和y坐标

这个方法返回元素在窗口坐标的位置,为了转换甚至用户滚动浏览器窗口以后仍然有效的文档坐标,需要加上滚动的偏移量:

            var box = e.getBoundingClientRect(); //获得视口在坐标中的位置
            var offsets = getScrollOffsets(); //上面定义的工具函数
            var x = box.left + offsets.x;
            var y = box.top + offsets.y;

在很多浏览器(和w3c标准中),getBoundingClientRect()对象还包含width和height属性,但在原始的ie中未实现,为了简便,可以这样计算width和height属性

            var box = e.getBoundingClientRect(); //获得视口在坐标中的位置
            var w = box.width || (box.right - box.left);
            var h = box.height || (box.bottom - box.top);

如果想查询每个独立的矩形,调用getClientRects()方法来获得一个只读的类数组对象,它的每个元素类似于getBoundingClientRect()返回的矩形对象

我们知道,例如getElementByTagName()这样的DOM方法返回的结果是“实时的”,当这些结果变化时能自动更新,但是etBoundingClientRect()和getClientRects()所返回的矩形对象(和矩形列表并不是实时的),他们只是调用方法时视觉状态的静态快照。用户在滚回或改变浏览器大小的时候并不会更新他们。

iii.判断元素在某点

getBoundingClientRect()方法使我们在视口中找到判定元素的文章,但有时我们反过来想,判定在视口中指定的位置有什么元素。这时我们可以用Document对象的elementFormPoint()方法来判定。传递(x,y)坐标,该方法在指定位置返回一个元素。由于该方法的算法还没有具体定下来,但是这个方法意图返回在那个点里和最上面z-index属性的元素。如果指定的点再视口以外,elementFromPoint()返回null.即使改点在转换为文档坐标后是完美有效的,返回值也一样

        //    <button id="button1">点击文档坐标(100, 100)所在元素文字颜色变红</button>
            document.getElementById("button1").onclick = function() {
                document.elementFromPoint(1000, 100).style.color = "#cd0000";
            };

9.HTML表单

HTML的<form>元素和各种各样的输入元素,如input,seclect和button。他们在客户端编程中有着重要的地位。这些html元素可以追溯到web最开始,比javascript更早。html表单就是第一代web应用程序背后的运行机制,它根不需要javascript。用户的输入从表单元素来收集;表单这些输入递交给服务器,服务器处理并生成一个新的HTML页面(通常有一个新的表单元素)并显示在客户端。

以下小节阐述 了用HTML表单如何做到这些事情。表单由HTML元素组成就像HTML文档的其它部分一样,可以用文章中介绍的DOM来操作它们。但是表单是第一批校本化的元素,在早期的客户端编程中它们还支持比DOM更早的其它API。

注意,本节是关于校本化的HTML表单,而不是HTML本身。假设你已经定义表单的HTML元素,(input textarea select)并有了解。下文的列出常用的表单元素,更详细的内容参考第四部分的表单和表单元素的API,在form、input、option、select、textarea、请参考下面。

HTML元素 类型属性 事件处理程序 描述和事件
input type="button"或button type="button" "button" onclick 按钮
input type="checkbox" "checkbox" onchange 复选按钮
input type="file" "file" onchange 载入web服务器的文件的文件名输入域;它的value是只读的
input type="hidden" "hidden" none 数据由表单提交,但对用户不可见
option none none Select对象中的单个选项,事件处理程序在select对象上,而非单独的Option对象上
input type="password" "password" onchange 密码输入框,输入的字符不可见
input type="radio" "radio" onchange 单选按钮,只能选一个
input type="reset"或button type="reset" "reset" onclick 重置表单的按钮
select "select-one" onchange 选项只能单选的列表或选项可多选的下拉菜单,另见option
select multiple select-multiple onchange 选项可以多选的列表,另见option
input type="submit"或button type="sumit" "submit" onclick 表单提交按钮
input type="text" "text" onchange 单行文本输入域;type属性缺少或无法识别时,默认的input元素
textarea "textarea" onchange 多行文本输入域

 i.选取表单和表单元素

表单和它们所包含的元素可以用如getElementById()和getElementByTagName()等标准方法从文档中来选取

            var fields = document.getElementById("address").getElementsByTagName("input");

支持querySelectorAll()的浏览器中,从一个表单中选取所有的单选按钮或所有同名的元素代码如下

             //id为"shipping"的表单中的单选按钮
            document.querySelectorAll('#shipping input[type="radio"]');
             //id为"shipping"的表单中所有name为"method"的单选按钮
            document.qierySelectorAll('#shipping input[type="radio"][name="method"]')

尽管如此,如同在12.7节,13.2.ii节和13.2.iii所描述的,有name或id属性的

<form>元素能够通过很多方法来选取。name="address"属性的form可以用下面任何的方法来选取

                window.address //不可靠,不要使用
                 document.address //仅当表单有name属性时可用
                 document.forms.address //仅当方法有name或id的表单
                 document.forms[n] //不可靠:n是表单的序号

13章2节第3小节阐述了document.forms是一个HTMLCollection对象,可以通过数字序号或id或id来选取表单元素。Form对象的本身行为类似多个表单元素组成的HTMLCollection集合,也可以通过那么或数字来索引。如果名为"address"的表单的第一个元素的name是"street",可以使用以下任何的一种表达式来引用该元素

            document.forms.address[0];
            document.forms.address.street;
            document.address.street //当有name = "address",而不是id="address"

一般来说指定文档元素的方法用id属性要比name属性更佳。但是,name属性在html表单提交中有特殊的目的,它在表单中较为常用,在其它元素中少用。它应用与相关的浮现按钮组合强制共享的name属性值的、互斥的单选按钮组。请记住,当那么用来索引一个HTMLCollection对象并并且包含多个元素来共享name时,返回值为一个类数组对象,它包含所有匹配的元素。考虑以下表单,它包含多个单选按钮来选择运输方式。

        <form name="shipping">
            <fieldset>
                <legend>shipping method</legend>
                <label>
                    <input type="radio" name="method" value="1st">第一次</label>
                <label>
                    <input type="radio" name="method" value="2day">第二次</label>
                <label>
<input type="radio" name="method" value="3rd">第三次</label>
            </fieldset>
        </form>

对于该表单,用如下代码来引用单选按钮的元素数组

            var methods = document.forms.shipping.elements.method;
            methods[0].name // method
            methods[0].value //1st

注意,<form>元素本身有一个HTML属性和对于的javascript属性叫"method",所有在此案例中,必须要用该表单的elements属性而非method属性。为了判定用户的那种运输方式,需要遍历数组中的表单元素,并检测他们的checked属性:

                var shipping_method;
                for (var i = 0; i < methods.length; i++)
                    if (methods[i].checked) shipping_method = method[i].value;

在下一节中可以看到更多表单元素的属性,如checkedvalue

ii.表单和元素的属性

上面描述的elements[]数组是Form对象中最有趣的属性 。Form对象中其他属性相对没有如此重要。action、encoding、method和target属性(property)直接对应于<form>元素的action、encoding、method个target等HTML属性(attribute)。这些属性都控制了表单是如何来提交数据倒web服务器并如何显示的。客户端javascript能够设置这些属性值,不过仅当表单真的会将数据提交到一个服务端程序时它们才有用。

在javascript产生之前,要有一个转义的“提交”按钮来提交表单,用一个专用的“重置”按钮来重置各元素的值。javascript的Form对象支持两个方法:submit()和reset(),它们完成同样的目的。调用Form对象的submit()方法来提交表单,调用reset()方法来重置表单元素的值。

type
标识表单元素类型的只读的字符串。征对<input>标签定义的表单元素而言,就是其type属性的值。其它调单元素(如<textarea>和<select>)定义type属性是为了轻松标识它们。与input元素在类型检测时相互区别。
form
对包含元素Form对象的只读引用,或者如果元素没有包含在一个<form>元素中的值则为null
name
只读的字符串,由HTML属性name指定
value
可读/写的字符串,指定了表单元素包含或代表的“值”。它就是当提交表单时发送到web服务器的字符串,也是javascript程序有时候会感兴趣的内容。征对Text和Textarea元素,该属性值包含了用户输入的文本。征对用<input>标签创建的按钮元素(除了用<button>标签创建的按钮),该属性值指定了按钮显示的文本。但是,征对单选和复选按钮,该属性用户不可见也不能编辑。它仅是用HTML的value属性来设置一个字符串。它在表单提交的时候使用,但在关联表单元素的额外数据时也很有用。在本章后面有关不同类目的表单元素小节中将深入讨论value属性。

iii.表单和元素的事件处理程序

每个form元素都有一个onsubmit事件处理来完成侦测表单的提交。还有一个onreset事件进行表单侦测重置。表单提交前调用onsubmit程序;它通过返回false能取消提交动作。这给javascript程序一个机会检查用户输入错误。目的是为了避免或不完整的数据提交到服务端程序。注意,onsubmit只能通过单击按钮来触发,直接调用表单的submit方法不触发onsubmit事件处理程序。

onreset事件处理程序和onsubmit是类似的。它在表单重置之前调用。它在表单重置之前调用,通过返回false能够阻止表单元素被重置。在表单中很少需要“重置”按钮,但如果有,你可能需要提醒用户来确认是否重置。

        <form>
            ...
            onreset = "return confirm('你确定重置所有的输入从新输入码?')"
            ...
        </form>

类似onsubmit事件处理程序,onreset它只能通过重置按钮来触发,直接调用表单的reset方法不能触发onreset事件处理程序。

当用户在表单元素交互时往往会触发click或change事件。通过定义onclick或onchange事件处理程序可以处理这些事件。一般来说,当按钮的表单元素激活(甚至通过键盘而不是实际的鼠标单击发生激活)时他们就会触发change事件。

表单在接收点键盘的焦点时也会触发 focus 事件,失去焦点时会触发blur事件

iiii.按钮

按钮是常见的表单元素之一。按钮的本身没有默认的行为,除非它有onclick实际处理程序。否则它没有什么用处。以<input>定义的按钮会将value属性的值以纯文本显示。当onclick事件所触发的动作可以概念化为“跟随此链接”是就用一个链接;否则用按钮

提交和重置元素本就是按钮不同的是它们之间有关联的默认动作(提交和重置)。

本书的第四部分末包含input按钮,关于按钮表单元素详细内容参看input项,它包含了用button元素创建的按钮。

iiiii.开关按钮

复选框和单选按钮,或有两种视觉状态的按钮:选中或未选中。用户可以单击它来改变它的开关状态。单选和复选框都定义了它的checked属性,改属性是可读/写的布尔值,它指定了元素当前是否选中。defaultChecked属性也是布尔值,它是HTML属性的checked值;它指定了元素第一次加载页面时是否被选中。

当用户单击或复选开关按钮,单选或复选框元素触发onclick事件。如果由于单击开关按钮改变了它的状态,它也触发onchange事件。

iiiiii.文本域

文本域是常见常用的元素,用户可以输入简短的文本字符串。value属性表示用户输入的内容。通过显式的设置属性值可以显示地指定应该在输入域中显示的文本。

在HTML5中,placeholder属性表示用户在输入框中提示的信息。

不同的文本输入元素定义onkeypress、onkeydown和onkeyup事件处理程序。

iiiiiii.选择框和选择元素

select元素可以让用户做出一组选择(Option元素表示),浏览器通常将其渲染为下拉菜单的形式。但当指定size属性大于1时,它将显式为列表中的选项(可能有滚动)。<select>元素可能有两种不同的方式运作,这取决于type的属性值是如何设置的。如果<select>元素有multiple属性,也就 是select对象的type属性值为"select-multiple",那就允许用户有多个选项,如果没有多选项属性,那只能选择单个项目,它的type属性值为“select-one”。

为Select元素增加一个新的选项,首先用Option()构造函数创建一个新的Option对象,然后将其添加到options[]属性中,代码如下:

             //创建一个新的属性
            var zaire = new Option("Zaire", // text属性
                "zaire", //value属性
                false, //defaultSelected属性
                false); //selected属性
                //通过添加到options数组中,在Select元素中现在改选项
                var countries = document.address.country;//得到Select对象
                countries.options[countries.options.length] = zaire;

请牢记这一点,这些专用的Select元素的API已经很老了。可以用那行标准的调用更明确的插入和移除选项元素:Document.createElement(),Node.insertBefore(),Node.removeChild()等。

10.其它文档特性

本章一开始就说明了它是最长的一章,也是最重要的一章。本章的最后一节涵盖了Document对象的若干混杂的特性。

cookie
允许javascript程序读、写HTTP cookie的特殊的属性。18章涵盖该属性

domain

该属性允许当前web页面之间的交互,相同域名下相互信任的web服务器之间协作放宽同源策略的安全限制(11章6节ii)

lastModified
包含文档修改的时间的字符串

referrer
如果有,它表示浏览器导航到当前链接的上一个文档。该属性和HTTP的Referer头部信息相同,只是拼写有两个r.

title
文档的<title></title>标签之间的内容

URL
文档的URL,只读字符串而不是Location对象。该属性与Location.href初始值相同,只是不包含Location对象的动态变化。例如,如果用户在文档中导向一个新的片段,Location.href会发生变化。

referrer是这些属性中最有趣的属性之一:它包含用户连接到当前文档的上一个文档的URL,可以用如下代码使用该属性:

            if (document.referrer.indexOf("http://www.google.com/search?") == 0) {
                var args = document.referrer.substring(ref.indexOf("?") + 1).split("&");
                for (var i = 0; i < args.length; i++) {
                    if (args[i].substring(0, 2) == "q=") {
                        document.write("<p>welcome google User.");
                        document.write("You searched for:" + unescape(args[i].substring(2)).replace('+', ''))
                            break;
                        }
                    }
                }

ii.document.write()方法

document.write()方法是其中一个由Netscape2浏览器实现的早期脚本话API,也曾经是问道显示计算后的文本的唯一代码。新带代码已经不需要它了,但在已有的代码中还能看到它的方法。

document.write()方法会将其的字符串连接起来,然后让结果字符串插入到文档中调用它的脚本元素的位置。如下

            document.write("<p>document title :" + document.title);
            document.write("<p>URL :" + document.URL);
            document.write("<p>Referred:" + document.referrer);
            document.write("<p>Modified on :" + document.lastModified);
            document.write("<p>Accessed :" + new Date());

只有解析文档的时候才能使用write()方法输出HTML到文档中,理解这点非常重要,也就是说能在<script>元素的顶层代码中调出document.write(),就是因为这些脚本是文档解析流程的一部分。

值得一提的是,Document对象还有支持writeln()方法。除了在其它参数输出之后加一个换行符以外它和wirte()方法完全一样。例如在<pre>元素内输出预格式化的文本时非常有用。

在当今的代码中document.wirte()方法并不常用,innerHTML属性和其它DOM技术提供了更好的方法增加 内容。另一方面,某些算法的确能够使得他们本身称为很好的I/O API。如同write()方法提供的API一样。如果你正在书写在运行时计算和输出文本代码,可能会下面的例子感兴趣,它利用指定元素的innerHTML书写包装了简单的write()和close()方法。

             /*征对innerHTML属性的流式API*/
             //设置元素的innerHTML定义简单的“流式”API
            function ElementStream(elt) {
                    if (typeof elt === "string")
                        elt = document.getElementById(elt);
                    this.elt = elt;
                    this.buffer = "";
                }
                //连接所有的参数,添加到缓存中
            ElementStream.prototype.wirte = function() {
                this.buffer += Array.prototype.join.call(arguments, "");
            };
             //类似write(),只是添加了换行符
            ElementStream.prototype.writeln = function() {
                this.buffer += Array.prototype.join.call(arguments, "") + "\n";
            };
             //从缓存中设置元素的内容,然后清空缓存
            ElementStream.prototype.close = function() {
                this.elt.innerHTML = this.buffer;
                this.buffer = "";
            };

 iii.查询和选取的文本

有时判定用户在文档中选取了那些文本非常有用,可以用类似如下的函数达到目的。

            function getSelectedText() {
                if (window.getSelection) //HTML5标准API
                    return window.getSelection().toString();
                else if (document.selection) //IE独有技术
                    return document.selection.createRange().text;
            }

标准的window.getSelection()方法返回一个Selection对象,后者描述了当前选取的一系列一个或多个Range对象。Selection和Range定义了一个不常用的的较为复杂的API.本书并没有记录。toString()方法是Selection对象中的最重要的广泛实现了(除了IE)特性,他返回选取的纯文本。

IE定义了一个不同的API,它在本书中也没有文档记录。Document.selection对象代表了用户的选择。该对象的createRange()方法返回IE独有的TextRange对象,特的text属性包含了选取的文本。

如上的代码在书签工具(11.2.iiii)中特别有用,它选取操作的文本,然后利用搜索引擎或参考某个单词,例如,如下的HTML链接在Wikipedia上查找选取的文本,收藏书签后,该链接和它包含的javascript URL就编成了一个书签工具

<a href="javascript: var q;
if (window.getSelection) q = window.getSelection().toString();
else if (document.selection) q = document.selection.createRange().text;
void window.open('https://www.baidu.com/s?wd=' + q)">选取后点击链接查找</a>

格式化代码:

            javascript: var q;
            if (window.getSelection) q = window.getSelection().toString();
            else if (document.selection) q = document.selection.createRange().text;
            void window.open('https://www.baidu.com/s?wd=' + q);

上述的代码兼容性不佳:window对象的getSelection()方法无法返回那些表单元素input或textarea内部的文本,它只选取主体文本。另外,iE的Document.selection属性可以返回任意地方上选取的文本。

从文本输入域或textarea输入的元素可以获取的文本可以使用如下代码

        elt.value.substring(elt.selectionStart,elt.selectionEnd);

IE8以及更早的版本浏览器不支持selectionStart,selectionEnd属性。

iiii.可编辑的内容

我们已经知道HTML表单元素包含了文本字段和文本域元素,用户库输入并编辑纯文本。随着IE的脚本,所有当今的web浏览器也开始支持简单的HTML编辑功能;你已经看到在这个页面上使用了,如博客评论。它嵌入了一个文本编辑器,包含了一系列的按钮工具栏来设置排版样式等。

有两种办法启用编辑功能。其一,设置任何标签的HTML contentEditable属性;其二,设置对于元素的javascript contentEditable属性,这都是使元素的内容变得可编辑,当用户点击该元素的内容就会出现插入光标。用户敲击键盘就 可以将内容插入其中。如下

        <div id="editor" contenteditable>
        click to edit
        </div>

浏览器可以给表单字段和contenteditable元素支持自动拼写检查。在支持该功能的浏览器中,检查可能默认的开启或关闭。为元素添加spellcheck元素来显式的开启拼写检查

而使用spellcheck=false来显式关闭该功能(例如在一个textarea将显式源代码或其它内容包含了字典找不到的表示时。)

将Document对象的designMode属性设置为字符串"on"是的整个文档可编辑。(设置为off将恢复为只读文档),designMode属性并没有对应的HTML属性

如下代码使得<iframe>内部文档可编辑

             //<iframe id="editor" src="about:blank"></iframe>
            window.onload = function() {
                var editor = document.getElementById("editor");
                editor.contentDocument.designMode = "on"; //开启编辑
            }

浏览器定义了多项文本编辑器命令,大部分没有键盘快捷为了执行这些命令,应该使用Document对象execCommand()方法。(注意,这里是Document方法,而不是设置了contenteditable属性的元素方法。如果文档的元素中有多个可编辑的元素,命令将字段应用到选取或插入光标的那个元素上),用execCommand()执行的都是如"bold","subscript","justifycenter","或"insertimage"之类的字符串。命令名是execCommand()第一个参数。有些命令还需要一个值的参数,例如:"createlink"需要一个超级链接URL。理论上,execCommand()第二个参数为true,浏览器会自动提示用户输入的所需值。但为了提高可移植性,你应该提示用户输入,并传递false为二个参数,传递用户输入的值作为第三个参数。

            function bold() {
                document.execCommand("blod", false, url);
            }

            function link() {
                var url = prompt("输入link描述");
                if (url) document.execCommand("createlink", false, url)
            }

execCommand()所支持的命令通常是工具栏上的按钮触发的。当要触发的命令不可用时,良好的UI会使对应的按钮无效。可以给document.queryCommandSupport()传递命令查询浏览器是否支持该命令。(例如,一条需要文本选择区域的命令在无选区的情况下,可能是无效的)有一些 命令,如"bold"和"italic"有一个布尔值状态,开关处于当前选区或可以使用document.queryCommandState()。最后,有些命令,(如“fontname”)有一个相关的值(字体系列名)。用document.queryCommandValue()查询该值。如果当前文本选区了两种不同的字体,"fontname"是不确定的。使用document.querycpmmandIndeterm()来检测这种情况。

不同的浏览器实现了不同的命令组合,只有一少部分得到了很好的支持。如“bold”,“italic”,“createlink”,“undo”和“redo”(互操作命令表,请参考http://www.quirksmode.org/dom/execCommand.html),HTML5草案定义了以下命令,但他们还没有没被普遍的支持,就不做详细的文档记录:

blod insertLineBreak slectAll
createLink insertOrderedList subscript
delete insertUnorderedList superscript
formatBlock insertParagraph undo
forwardDelete insertText unlink
insertImage italic unselect
insertHTML redo  

如果web需要一个富文本编辑器,就需要采纳一个预先构建的解决浏览器差异的方法。

一旦用户编辑了某元素,该元素的设置了conteneditable属性,就可以使用innerHTML属性得到已编辑内容的HTML标记。如何处理富文本自己决定(YUI和Dojo框架包含了编辑器组件,还有一些可选的方案,参考:http://en.wikipedia.org/wiki/online_rich-text_editor)。可以把它存储在隐藏的表单字段中。并通过提交表单发送到服务器

可以使用16章描述的技术直接把已编辑的文本发送到服务器。或者使用18章的计算在本地保存用户的编辑文本。

 

(本文完结,可以浏览学习前一节内容 第十二章:window对象,欢迎大家关注下章 第十四章:脚本化CSS

 

posted on 2015-03-04 00:43  村长很忙  阅读(1367)  评论(0编辑  收藏  举报