美文网首页
DOM2 和 DOM3

DOM2 和 DOM3

作者: 了凡和纤风 | 来源:发表于2019-08-02 17:38 被阅读0次

    本章内容:DOM2 和 DOM3 的变化、操作样式和 DOMAPI、DOM遍历与范围

    DOM1 级主要定义的是HTML 和 XML 文档的底层结构。DOM2 和 DOM3 级则在这个结构的基础上引入了更多的交互功能,也支持了更高级的XML特性。
    DOM2 和 DOM3 的模块

    • DOM2级核心(DOM Level 2 Core):在1级核心基础上构建,为节点添加更多的方法和属性。
    • DOM2级视图(DOM Level 2 Views):为文档定义了基本样式信息不同的视图。
    • DOM2级事件(DOM Level 2 Events):说明了如何使用事件与DOM文档交互。
    • DOM2级样式(DOM Level 2 Style):定义了如何以变成的方式来访问和编辑css样式信息。
    • DOM2级遍历和范围(DOM Level 2 Traversal and Range):引入了遍历DOM文档和选择其特定部分的新接口。
    • DOM2级HTML(DOM Level 2 HTML):在1级HTML基础上后见,添加了更多属性、方法和新接口。

    一、DOM变化

    DOM2级和3级的目的在于扩展DOM API,满足操作文档的需求,同时提供更好的错误处理及特性检测能力。

    为了确定浏览器是否支持这些DOM模块,可以使用 hasFeature() 来检测它们(这个方法在上一章提到过。)

    var supportsDOM2Core = document.implementation.hasFeature('Core', '2.0')
    var supportsDOM3Core = document.implementation.hasFeature('Core', '3.0')
    var supportsDOM2HTML = document.implementation.hasFeature('HTML', '2.0')
    var supportsDOM2Views = document.implementation.hasFeature('Views', '2.0')
    var supportsDOM2XML = document.implementation.hasFeature('XML', '2.0')
    

    1.1、针对XML 命名空间的变化

    有了XML命名空间,不同XML文档的元素就可以混合在一起,共同构成格式良好的文档。HTML不支持XML命名空间,但XHTML支持XML命名空间。以下给出实例中,皆为XHTML文档格式

    命名空间要使用xmlns特性来制定,并应将其包含在<html>元素中。

    <html xmlns="http://www.w3.org/1999/xhtml">
      <head>
        <title>Example XHTML page</title>
      </head>
      <body>
        Hello World
      </body>
    </html>
    

    在这个例子中,所有的元素都默认被视为XHML 命名空间的元素。要明确地为XML命名空间创建前缀,可以使用xmlns 后跟冒号,再跟前缀。

    <xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
      <xhtml:head>
        <xhtml:itle>EX XHTML Page</xhtml:title>
      </xhtml:head>
      <xhtml:body>
        Hello World
      </xhtml:body>
    </xhtml:html>
    

    有时候为了避免不同语言间的冲突,也需要使用命名空间来限定特性。

    <xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
      <xhtml:head>
        <xhtml:title>EX XHTML Page</xhtml:head>
      </xhtml:head>
      <xhtml:body xhtml:class="home">
        Hello World
      </xhtml:body>
    </xhtml:html>
    

    在只基于一种语言编写XML文档的情况下,命名空间实际上也没有什么用。不过,在混合使用两种语言的情况下,命名空间的用处就非常大了。
    比如下面这个混合了 XHML 和 SVG 语言的文档。

    <html xmlns="http://www.w3.org/1999/xhtml">
      <head>
        <title>Ex XHTML Page</title>
      </head>
      <body>
        <svg xmlns="http://www.w3.org/2000/svg" version="1.1"
             viewBox="0 0 100 100" style="width: 100%;height: 100%">
          <react x="0" y="0" width="100" height="100" style="fill:red" />
        </svg>
      </body>
    </html>
    

    通过设置命名空间,将<svg>标识为了与包含文档无关的元素。<svg> 所有子元素以及所有的特性,都被认为是 http://www.w3.org/2000/svg 命名空间

    1.1.1、Node类型的变化

    在 DOM2 级中, Node 类型包含下列特定命名空间的属性。

    • localName: 不带命名空间前缀的节点名称
    • namespaceURL:命名空间 URL 或者(在未指定的情况下是)null。
    • prefix:命名空间前缀或者(在未指定的情况下是)null

    当节点使用命名空间前缀时,其nodeName等于 prefix+ ":" + localName。

    <html xmlns="http://www.w3.org/1999/xhtml">
      <head>
        <title>Ex XHTML Page</title>
      </head>
      <body>
        <s:svg xmlns:s="http://www.w3.org/200/svg" version="1.1"
               viewBox="0 0 100 100" style="width: 100%; height: 100%">
          <s:react x="0" y="0" width="100" height="100" style="fill:red" />
        </s:svg>
      </body>
    </html>
    

    以上面这段代码为例。

    DOM3 级在此基础上更进一步,又引入了下列与命名空间相关的方法。

    • isDefaultNa mespace(namespaceURI):在指定的 namespaceURI是当前节点的默认命名空间的情况下返回 true。
    • lookupNamespaceURI(prefix):返回给定 prefix 的命名空间。
    • lookupPrefix(namespaceURI):返回给定 namespaceURI的前缀。

    针对前面的 代码调用 以上API

    console.log(document.body.isDefaultNamespace('http://www.w3.org/1999/xhtml')) // true
    
    var svg = document.getElementsByTagName('s:svg')[0] // 获取 svg的 引用
    console.log(svg.lookupPrefix('http://www.w3.org/2000/svg')) // s
    console.log(svg.lookupNamespaceURI('s')) // http://www.w3.org/2000/svg
    
    1.1.2、Document 类型的变化

    DOM2 级中的 Document 类型 包含下列与命名空间相关的方法:

    • createElementNS(namespaceURI, tagName):使用给定的 tagName 创建一个属于命名空间 namespaceURI 的新元素
    • createAttributeNS(namespaceURI, attributeName):使用给定的 attributeName 创建一个属于命名空间 namespaceURI 的新特性。
    • getElementsByTagNameNS(namespaceURI, tagName):返回属于命名空间 namespaceURI 的tagName 元素的 NodeList
    // 创建一个 新的 SVG 元素
    var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
    
    // 创建一个属于 某个命名空间 的新特性。
    var att = document.createAttributeNS('http://www.somewhere.com', 'radom')
    
    // 取得所有 XHTML 元素
    var eles = document.getElementsByTagNameNS('http://www.w3.org/1999/xhml', '*')
    

    只有在文档中存在两个或多个命名空间时,这些与命名空间有关的方法才是必须的。

    1.1.3、Element类型

    DOM2级核心 中有关Element的变化,主要涉及操作特性。新增的方法如下。

    • getAttributeNS(namespaceURI, localName):取得属于命名空间 namespaceURI 且 名为 localName 的特性
    • getAttributeNodeNS(namespaceURI,localName):取得属于命名空间 namespaceURI 且名为 localName的特性节点。
    • getElementsByTagNameNS(namespaceURI, tagName):返回属于命名空间 namespaceURI 的tagName 元素的 NodeList
    • hasAttributeNS(namespaceURI, localName):确定当前元素是否有一个名为 localName 的特性,而且该特性的命名空间是 namespaceURI。注意:"DOM2 级核心"也增加了一个 hasAttribute() 方法,用于不考虑命名空间的情况。
    • removeAttributeNS(namespaceURI, localName):删除属于命名空间 namespaceURI 且名为 localName 的特性
    • setAttributeNS(namespaceURI, qualifiedName, value):将属于命名空间 namespaceURI 且名为 qualifiedName 的特性 将其值设置为 value
    • setAttributeNodeNS(attNode):设置属于命名空间namespaceURI的特性节点。
      除了第一个参数外,这些方法与 DOM1级中相关方法的作用相同;第一个参数始终都是一个命名空间URI
    1.1.4、NamedNodeMap 类型的变化

    NameNodeMap 类型也新增了下列与命名空间有关的方法。由于特性是通过 NameNodeMap 表示的,因此这些方法多少情况下只针对特性使用。

    • getNamedItemNS(namespaceURI, localName):取得属于命名空间 namespaceURI 且名为 localName 的项。
    • removeNamedItemNS(namespaceURI, localName):移除属于命名空间 namespaceURI 且民委 localName 的项。
    • setNamedItemNS(node):添加 node,这个节点已经事先指定了 命名空间的信息。

    由于一般都是通过 元素 访问特性,所以这些方法很少使用。

    1.2、其他方面的变化

    这些变化与 XML 命名空间无关,而是更倾向于确保 API 的可靠性及完整性。

    1.2.1、DocumentType 类型的变化

    DoucmentType 类型新增了 3个属性:publicIdststemIdinternalSubset。前两个属性表示的是文档类型声明中的两个信息段。

    <!DOCTYPE HTML PUBLIC "-//W3C/DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
    
    console.log(document.doctype.publicId) // -//W3C/DTD HTML 4.01//EN
    console.log(document.doctype.systemId) // http://www.w3.org/TR/html4/strict.dtd
    

    internalSubset 用于访问包含在文档类型中的额外定义。在HTML中极少用到,在XML中常用一些。

    1.2.2、Document 类型的变化

    Document类型变化中唯一于 命名空间无关的方法是 importNode()这个方法的用途是从一个文档中取得一个节点,然后将其导入到另一个文档,使其成为这个这个文档的一部分。需要注意的是,每个节点都有一个 ownerDocument 属性,表示所属文档。如果调 appendChild() 是传入的节点属于 不同文档(ownerDocument的值不一样),则会导致错误。在调用 importNode()时传入不同文档的节点则会返回一个新节点,这个新节点的所有权归当前文档所有。
    importNode() 方法和 cloneNode() 方法非常相似,接受两个参数:要复制的节点、是否复制子节点,返回原来节点的副本。

    var newNode = document.importNode(oldNode, true) // 深复制,复制子节点
    document.body.appendChild(newNode)
    

    同样这个方法在HTML中并不常用,在XML中使用较多。


    “DOM2级视图” 模块添加了一个 名为 defaultView 的属性,其中保存着一个指针,指向拥有给定文档窗口(或框架)。IE不支持此属性,IE中 的等价属性时 prentWIndow(Opera也支持这个属性)。
    确定文档的归属窗口

    var parentWindow = document.defaultView || document.parentWindow
    

    "DOM2级核心" 还为 docunebt.implementation 对象规定了两个新方法:

    • createDocumentType(): 用于创建一个 新的 DocumentType 节点,接 受以下参数:
      • 文档类型名称
      • publicId
      • systemId
    var doctype = document.implementation.createDocumentType('html', '-//W3C/DTD HTML 4.01//EN', 'http://www.w3.org/TR/html4/strict.dtd')
    

    由于既有文档类型不能改变,因此这个方法只在创建新文档时有用

    • createDocument():用于创建一个新文档,接受三个参数:
    • 针对文档元素的 namespaceURI
    • 文档元素的标签名
    • 文档类型
    // 创建一个新的XML文档
    var xmlDoc = document.implementation.createDocument('', 'root', null)
    
    // 创建一个XHML 文档
    // 这里的 doctype 引用 上面例子中 通过 createDocumentType创建的
    var xhtmlDoc = document.implementation.createDocument('http://www.w3.org/1999//xhtml', 'html', doctype)
    

    "DOM2级HTML" 模块 页为 document.implementation 新增了一个方法,createHTMLDocument(),改方法创建一个完整的 HTML 文档。接受一个参数:即新创建文档的标题(<title>中间的文本)。

    var htmlDoc = document.implementation.createHTMLDocument('New Doc')
    

    通过 createHTMLDocument() 创建的这个文档,是HTMLDocument类型的实例,具有该类型的所有属性和方法包括 title 和 body 属性。 这个方法存在兼容问题

    1.2.3、Node类型的变化

    添加了 isSupported() 方法。与 DOM1级的 hasFeature() 方法类似。isSupported() 方法用于确定当前节点具有什么能力。接受两个参数:特性名 和 特性版本号;返回一个布尔值。

    if (document.body.isSupported('HTML', '2.0')) {
      // todo
    }
    

    与 hasFeature() 类似,建议在 确定某个特性是否可用时,最好还是使用能力检测


    DOM3级 引入了 两个辅助比较节点的方法:isSameNode() 和 isEqualNode()。这两个方法都接受一个节点参数,

    • isSameNod():传入节点与引用节点相同时(两个节点引用的是同一个对象)返回 true。
    • isEqualNode():传入节点与引用节点相等时(相同的类型,具有相等的属性,并且 attributes 和 childNodes 也相等)返回true
    var div1 = document.createElement('div')
    div.setAttribute('class', 'box')
    
    var div2 = document.createElement('div')
    dvi2.setAttribute('class', 'box')
    
    
    console.log(div1.isSameNode(div1)) // true
    console.log(div1.isEqualNode(div2)) // true
    // 引用的不是同一个对象,不相同
    console.log(div1.isSameNode(div2)) // false
    
    1.2.4、框架的变化

    DOM2级中有一个新属性:contentDocument,这个属性包含一个指针,指向表示框架内容的文档对象。IE8之前不支持 这个属性。但支持一个 contentWindow 的属性,该属性返回框架的 window 对象,而这个window 对象又有一个 document 属性。

    // 访问内嵌框架的文档对象
    var iframe = document.getElementById('myIframe')
    var iframeDoc = iframe.contentDocument || iframe.contentWindow.document
    

    所有浏览器都支持 contentWindow属性

    二、样式

    "DOM2级样式"围绕样式机制提供了一套API。可以通过 hasFeature() 检测是否支持。

    var supportsDOM2CSS = document.implementation.hasFeature('CSS', '2.0')
    var supportsDOM2CSS2 = document.implementation.hasFeature('CSS2', '2.0')
    

    2.1、访问元素的样式

    任何支持style 特性的 HTML 元素在 JavaScript中都有对应的 style 属性。这个 style 对象是 CSSStyleDeclaration 的实例。包含HTML的style 特性指定的所有样式信息。
    css 中的 '-' 在JavaScript中要变成 驼峰式,比如:background-color = backgroundColor

    var myDiv = document.getElementById('div')
    
    // 设置背景颜色
    myDive.style.backgroundColor = 'cyan'
    
    // 改变大小
    myDiv.style.width = '200px'
    
    // 指定边框
    myDiv.style.border = '1px solid black'
    

    同时通过 style 对象也可以获取 style特性中指定的 样式

    // 获取背景色
    console.log(myDiv.style.backgroundColor)
    
    2.1.1、DOM样式属性和方法

    "DOM2级样式"规范还未 style 对象定义了一些属性和方法。

    • cssText:能够访问到 style 特性中的 css代码
    • length:应用元素的CSS属性的数量
    • parentRule:表示CSS信息的CSSRule对象
    • getPropertyPriority(propertyName):如果给定的属性使用了 !important 设置,则返回 'important';否则,返回空字符串
    • getPropertyValue(propertyName):返回给定属性的字符串值。
    • item(index):返回给定位置的CSS属性的名称。
    • removeProperty(propertyName):从样式中删除给定属性
    • setProperty(propertyName, value, priority):将给定属性设置为相应的值,并加上优先权标志('important'或一个空字符串)。

    通过 cssText属性可以访问style 特性中的代码。并且如果通过 cssText 修改 特性,会重写整个 style 特性的值。

    // 设置
    myDiv.style.cssText = 'width: 24px; height: 30px'
    // 获取
    console.log(myDive.style.cssText)
    

    length属性 与 item()方法配套使用,以便在迭代元素中定义的css属性,在使用length 和 item() 时,style 对象实际上就相当于一个集合,可以使用方括号语法来代替item()

    for (var i = 0, len = myDiv.style.length; i < len; i++) {
      console.log(myDiv.style[i]) // myDiv.style.item(i)
    }
    

    通过如上遍历可以取得所有的特性名,然后通过 getPropertyValue() 获取获取每一个特性值

    var prop, value, i, len
    for(i = 0, len = myDiv.length; i < len; i++) {
      prop = myDiv.style.item(i)
      value = myDiv.style.getPropertyValue(prop)
      console.log(prop + ": " + value )
    }
    

    getPropertyValue() 返回的始终是CSS属性值的字符串表示。
    使用getPropertyCSSValue()方法,返回一个对象。这个对象又两个属性:

    • cssText
      这个属性的值与 getPropertyValue() 返回的值相同
    • cssValueType
      返回一个数值常量:
      • 0:表示继承的值
      • 1:表示基本的值
      • 2:表示值列表
      • 3:表示自定义的值
    var i, prop, value, len
    for(i = 0, len = myDiv.length; i < len; i++) {
      prop = myDiv.style.item(i)
      value = myDiv.style.getPropertyCSSValue(prop) // 获取 CSSValue 对象
      console.log(prop + ': ' + value.cssText + '(' + value.cssValueType + ')') 
    }
    

    要从元素的样式中移除某个 CSS 属性,需要使用 removePropertyValue() 方法。这个方法移除一个属性

    myDiv.style.removeProperty('border')
    
    2.1.2、计算的样式

    "DOM2级样式"增强了 document.defaultView,提供了 getComputedStyle() 方法。这个方法接受两个参数:要取得计算样式的元素、一个伪元素字符串(例如 :after)如果不需要伪元素可以设置为 null。 返回一个 CSSStyleDeclaration 对象。

    <html>
      <head>
        <title>EX</title>
        <style>
          #div {
            color: pink;
            width: 20px;
            height: 30px;
          }
        </style>
      </head>
      <body>
        <div id="div" style="color: pink">
      </body>
    </html>
    
    var myDiv = document.getElementById('div')
    var computedStyle = document.defaulltView.getComputedStyle(myDiv, null)
    console.log(computedStyle .color) // rgb(255, 192, 203)
    console.log(computedStyle .width) // 20px
    console.log(computedStyle .height) // 30px
    

    IE不支持 getComputedStyle() 方法。 在IE中,每个具有 style 属性的元素还有一个 currentStyle 属性。这个属性是CSSStyleDeclaration的实例,包含当前元素全部计算后的样式。

    var computedStyle = myDiv.currentStyle
    
    console.log(computedStyle.width) // 20px
    

    在所有浏览器中,所有计算后的样式都是只读的;不能修改计算和样式对象中的css属性。

    2.2、操作样式表

    CSSStyleSheet类型表示的是样式表,是一套只读的接口,CSSStyleSheet 继承自 StyleSheet,从 StyleSheet接口继承而来的属性如下。

    • disabled: 表示样式表是否被禁用的布尔值。这个属性是 可读/写的,将这个属性设置为 true,从以禁用样式表。
    • href:如果样式表是通过<link>包含的,则是样式表的URL;否则,是null
    • media:当前样式表支持的所有媒体类型的集合。与所有 DOM 集合一样,这个集合也有一个length 属性和 一个 item() 方法。也可以使用方括号语法取得集合中特定的项。如果集合是空列表,表示样式表适用于所有媒体。在IE中,meida是一个反映 <link> 和 <style> 元素的 media 特性值的字符串。
    • ownerNode:指向拥有当前样式表的节点的指针,样式表可能是在 HTML 中通过 <link> 或 <style> 引入的。如果当前样式表通过 @import 导入,则这个属性值为 null。IE不支持这个属性
    • parentStyleSheet:在当前样式表是通过 @import 导入的情况下,这个属性是一个指向导入它的样式表的指针。
    • title:ownerNode 中 title 属性的值。
    • type:表示样式表类型的字符串。对CSS样式表而言,这个字符串是"type/css"

    除了disable 属性之外,其他属性都是只读的。在支持以上所有这些属性的基础上,CSSStyleSheet还支持下列的属性和方法

    • cssRules:样式表中包含的样式规则的集合。IE不支持这个属性,但有一个类似的rules属性
    • ownerRule:如果样式表是通过@import 导入的,这个属性就是一个值正,指向表示导入的规则;否则,值为null。IE不支持这个属性。
    • deleteRule(index):删除 cssRules 集合中指定位置的规则。IE不支持这个方法,但支持一个类似的 removeRule() 方法。
    • insertRule(rule, index):向cssRules 集合中指定的位置插入 rule 字符串。IE不支持这个方法,类似的支持一个 addRule() 方法。

    应用于文档的所有样式表是通过 document.styleSheets 集合来表示的`

    var sheet = null
    for (var i = 0, len=document.styleSheets.length; i < len; i++){ // 遍历 样式表
      sheet = document.styleSheets[i]
      console.log(sheet.href) // 访问 href 属性
    }
    

    不同浏览器的 document.styleSheets 返回的样式表也不同。所有浏览器都会包含<style>元素 和 rel 特性被设置为 ”stylesheet“的<link>元素引入的样式表。IE 和 Opera也包含 rel 被设置为 "alternate stylesheet"的<link>元素的引入的样式表。

    也可以直接通过<link> 或 <style> 元素取得 CSSStyleSheet。DOM规定了一个包含 CSSStyleSheet 对象的属性,名叫 sheet;IE支持的是 styleSheet 属性。

    // 在不同浏览器中取得 样式对象
    function getStyleSheet(element) {
      return element.sheet || element.styleSheet
    }
    
    // 取得第一个 <link> 元素引入的样式表
    var link = document.getElementsByTagNames('link').item(0)
    var sheet = getStyleSheet(link)
    

    这里的 getStyleShee() 返回的样式表对象 于 document.styleSheets 集合中样式表对象相同。

    2.2.1、CSS规则

    CSSRule对象表示样式表中的每一条规则。实际上,CSSRule 是一个供其他多种类型继承的基类型,其中最常见的就是 CSSStyleSheet类型,表示样式信息。CSSStyleRule对象包含下列属性。

    • cssText:返回整条规则对应的文本。由于浏览器对样式表的内部处理方式不同,返回的文本可能会于样式表中实际的文本不一样; Safari 始终都会将文本转换成全部小写。IE不支持这个属性。
    • parentRule:如果当前规则是导入的规则,这个属性引用的就是导入的规则;否则,这个值为 null。IE不支持这个属性。
    • parentStyleSheet:当前规则所属的样式表。IE不支持这个属性。
    • selectorText:返回当前规则的选择符文本。由于浏览器对样式表的内部处理方式不同,返回的文本可能于样式表中实际的文本不一样。只读属性(除了Opera)
    • style:一个CSSStyleDeclaration对象,可以通过它设置和取得规则中特定的样式值。
    • type:表示规则类型的常量值。对于样式规则,这个值是1。IE不支持这个属性

    其中最常用的是cssText、selectorText、style
    cssText 属性与 style.cssText 属性类似,但并不相同。前者包含选择符文本和围绕样式信息的花括号,后者只包含样式信息(类似于元素的 style.cssText)。此外,cssText是只读的,style.cssText 也可以被重写。

    大多数情况下,使用style属性就可以满足所有操作样式规则的需求了。这个对象就像每个元素上的style属性一样,可以通过它读取和修改规则中的样式信息。

    div.box {
      background-color: blue;
      width: 100px;
      height: 200px
    }
    

    假设上面这条规则位于页面中的第一个样式表中

    var sheet = document.styleSheets[0]
    var rules = sheet.cssRules || sheet.rules // 取得规则列表
    var rule = rules[0] // 取得第一条 规则
    
    console.log(rule.selectorText) // div.box
    console.log(rule.style.cssText) // 完整的CSS代码
    console.log(rule.style.backgroundColor) // blue || rgb(..)
    

    也可以像下面这样来修改信息

    var sheet = document.styleSheets[0]
    var rules = sheet.cssRules || sheet.rules
    var rule = rules[0]
    rule.style.backgroundColor = 'red'
    

    需要注意的是,以这种方法修改会影响页面中适用该规则的所有元素

    2.2.2、创建规则

    inertRule()
    要向现有样式表中 添加新规则,需要使用 inertRule()方法,这个方法接受两个参数;规则文本、表示在哪里插入的规则的索引:

    sheet.insertRule('body { background-color: silver}', 0)
    

    插入的规则将成为样式表中的第一条规则——规则的次序在确定层叠之后应用到文档的规则时至关重要。Firefox、Safari、Opera 和 Chrome 都支持 这个方法。
    IE8及更早版本中,类似的支持 addRule() 方法;

    • 接受两个必选参数:
      • 选择符文本
      • css样式信息
    • 一个可选参数
      • 插入的位置。
    sheet.addRule('body', 'background-color: silver', 0) // 仅对IE有效
    

    IE中说明,关于这个方法最多可以使用添加 4095条样式规则。超出这个上限的调用将会导致错误。

    要跨越浏览器项样式表中插入规则,可以使用下面的函数。接收四个参数:要向其中添加规则的样式表、选择符文本、css样式信息、插入位置

    function insertRule(sheet, selectorText, cssText, position) {
      if (sheet.insertRule) {
        sheet.insertRule(selectorText + '{' + cssText + '}', position)
      } else if (sheet.addRUle) { // IE
        sheet.addRule(selectorText, cssText, position)
      }
     }
    
    insertRule(document.styleSheets[0], 'body', 'background-color: cyan', 0)
    

    如果要添加较多的规则,这样的方法就会比较繁琐。因此,在需要大量添加规则的时候,推荐使用前面提到的 动态加载样式表的技术。

    2.2.3、删除规则

    从样式表中删除规则
    deleteRule() 接受一个参数:要删除的规则的位置
    IE支持removeRule()接受一个参数:要删除的位置。

    function deleteRule(sheet, index) {
      if (sheet.deleteRule) {
        sheet.deleteRule(index)
      } else if(sheet.removeRule) { // IE
        sheet.removeRule(index)
      }
    }
    
    deleteRule(document.styleSheets[0], 0)
    

    与添加规则相似,删除规则也不是实际Web开发中常见的做法。

    2.3、元素大小

    最初DOM中没有规定如何确定页面中元素的大小。IE为此率先引入了一些属性,以便开发人员使用。目前所有主流浏览器都已经支持这些属性。

    2.3.1、偏移量

    元素的可见大小由其高度、宽度决定,包括所有内边距、滚动条和边框大小(注意,不包括外边距)。通过下列4给属性可以取得元素的偏移量

    • offsetHeight:元素在垂直方向上占用的空间大小,以像素计。包括元素的高度 、(可见的)水平滚动条的高度、上边框高度和下边框高度。
    • offsetWidth:元素在水平方向上占用的空间大小,以像素计。包括元素的宽度、(可见的)垂直滚动条的宽度、左边框宽度和右边框宽度。
    • offsetLeft:元素的左外边框至包含元素的左内边框之间的像素距离。
    • offsetTop:元素的上外边框包含元素的上内边框之间的像素距离。

    其中,offsetLeft 和 offsetTop属性与包含元素有关,包含元素的引用保存在 offsetParent 属性中。offsetParent 和 parentNode的值不一样相等(父元素,不一定就是包含引用指向的元素)

    偏移量图.png

    要想知道某个元素在页面上的偏移量,将这个元素的offsetLeft 和 offsetTop 与其 offsetParent的相同属性相加,如此循环直至根元素就可以得到一个基本准确的值了。

    // 到屏幕左边的偏移值
    function getElementLeft(element) {
      var actualLeft = element.offsetLeft
      var current = element.offsetParent
    
      while(current !== null) {
        actualLeft += current.offsetLeft
        current = current.offsetParent
      }
    
    return actualLeft
    }
    
    // 到屏幕顶部的偏移值
    function getElementTop(element) {
      if (element.offsetParent) {
        return arguments.callee(current) +  element.offsetTop
      }
      return  element.offsetTop
    }
    

    对于简单的CSS布局的页面,这两个函数可以得到非常精确的结构。

    2.3.2、客户区大小

    元素的客户区大小,指的是元素内容及其内边距所占据的空间大小。有关客户区大小的属于又两个:

    • clientWidth
      元素内容区宽度加上左右内边距宽度
    • clientHeight
      元素内容区高度加上上下内边距高度
      客户区大小.png
      客户区大小就是元素内部的空间大小,因此滚动条占用的空间不计算在内。最常用到这些属性的情况,就是确定浏览器视口大小的时候。
    function getViewport() {
      if (document.compatMode == 'BackCompat') { // 混杂模式
        return {
          width: document.body.clientWidth,
          height: document.body.clientHeight
        }
      } else {
        return {
          width: document.documentElement.clientWidth,
          height: document.documentELement.clientHeight
        }
      }
    }
    
    2.3.3、滚动大小

    滚动大小,指的是包含滚动内容的元素的大小。

    • scrollHeight:在没有滚动条的情况下,元素内容的总高度。
    • scrollWidth:在没有滚动条的情况下,元素内容的总高度。
    • scrollLeft:被隐藏在内容区域左侧的像素数。通过设置这个属性可以改变元素的滚动位置。
    • scrollTop:被隐藏在内容区域上方的像素数。通过设置这个属性可以改变元素的滚动位置。

    scrollWidth 和 scrollHeight 主要用于确定元素内容的实际大小。对于不包含滚动条的页面而言,scrollWidth 和 scrollHeight 与 clientWidth 和 clientHeight 之间的关系并不十分清晰。在这种情况下,基于 document.documentElement 查看这些属性会在不同浏览器间发现一些不一致问题。

    滚动大小

    在确定文档的总高度时(包括基于视口的最小高度时),必须取得 scrollWidth / clientWidth 和 scrollHeight / clientHeight 中的最大值,才能保证在跨浏览器的环境下得到精确的结果。

    var docHeight = Math.max(document.documentElement.scrollHeight, document.documentElement.clientHieght)
    var docWidth = Math.max(document.documentElement.scrollWidth, document.documentElement.clientWidth)
    

    对于运行在混杂模式下的IE,则需要用 document.body 代替 document.documentElement


    通过 scrollLeft 和 scrollTop 属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位置。
    下面这个函数会检测元素是否位于顶部,如果不是就将其回滚到顶部

    function scrollToTop(element) {
      if (element.scrollTop != 0) element.scrollTop = 0
    }
    
    2.3.4、确定元素大小

    浏览器为每个元素提供了一个 getBoundingClientRect()方法。这个方法返回一个矩形对象,包含四个属性:left、top、right、bottom这些属性给出了元素在页面中相对于视口的位置。
    浏览器的实现稍微有点不同。IE8及更早版本认为文档的左上角坐标是(2, 2),而其他浏览器包括 IE9 则将传统的(0, 0)作为起点坐标。在IE8及更早版本中,会返回(2, 2), 而其他浏览器返回(0, 0)

    // 元素距离当前视口的精确距离
    function getBoundingClientRect(element) {
       // 是否为初始化(是否定义过),避免多次执行
      if (typeof arguments.callee.offset != 'number') {
        var scrollTop = document.documentElement.scrollTop // 文档滚动的高度
    
        // 创建元素 测试左上角坐标
        var temp = document.createElement('div')
        temp.style.cssText = 'position:absolute; left: 0; top: 0;'
        document.body.appendChild(temp)
    
        // 计算 文档左上角的坐标
        arguments.callee.offset = -temp.getBoundingClientRect().top - scrollTop
        // 移除元素
        document.body.removeChild(temp)
        // 回收
        temp = null
      }
    
      var rect = element.getBoundingClientRect() // 距离当前视口的距离
      var offset = arguments.callee.offset // 文档左上角坐标的值
    
      return {
        left: rect.left + offset,
        right: rect.right + offset,
        top: rect.top + offset,
        bottom: rect.bottom + offset
      }
    }
    

    对于不支持 getBoundingClientReact()的浏览器,可以使用其他手段取得相同信息。

    // 对于不支持 getBoundingClientRect() 的浏览器
    function getBoundingClientRect(element) {
    
      var scrollTop = document.documentElement.scrollTop
      var scrollLeft = document.documentElement.scrollLeft
    
      if (element.getBoundingClientRect) {
        // 是否为初始化(是否定义过),避免多次执行
        if (typeof arguments.callee.offset != 'number') {
          var scrollTop = document.documentElement.scrollTop // 文档滚动的高度
    
          // 创建元素 测试左上角坐标
          var temp = document.createElement('div')
          temp.style.cssText = 'position:absolute; left: 0; top: 0;'
          document.body.appendChild(temp)
    
          // 计算 文档左上角的坐标
          arguments.callee.offset = -temp.getBoundingClientRect().top - scrollTop
          // 移除元素
          document.body.removeChild(temp)
          // 回收
          temp = null
        }
    
        var rect = element.getBoundingClientRect() // 距离当前视口的距离
        var offset = arguments.callee.offset // 文档左上角坐标的值
    
        return {
          left: rect.left + offset,
          right: rect.right + offset,
          top: rect.top + offset,
          bottom: rect.bottom + offset
        }
      } else {
        var actualLeft = getElementLeft(element) // 获取到屏幕左边的偏移值
        var actualTop = getElementTop(element) // 获取到屏幕上边的偏移值
    
        // 偏移值 - 滚动的距离 = 当前视口的值
        return {
          left: actualLeft - scrollLeft,
          right: actualLeft + element.offsetWidth - scrollLeft,
          top: actualTop - scrollTop,
          botoom: actualTop + element.offsetHeight - scrollTop
        }
      }
    }
    

    由于这里使用了 arguements.callee,所以这个模式不能再严格模式下使用

    三、遍历

    "DOM2级遍历和范围"模块定义了凉饿用于辅助完成顺序遍历DOM结构的类型:NodeIterator 和 TreeWalker。这两个类型能够基于给定的起点对DOM结构执行深度优化(depth-first)的遍历操作。
    可以通过下列代码检测浏览器对DOM2级遍历能力的支持

    var supportsTraversals = document.implementation.hasFeature('Traversal', '2.0')
    var supportsNodeIterator = (typeof document.createNodeIterator == 'function')
    var supportsTreeWalker = (typeof document.createTreeWalker == 'function')
    

    DOM遍历是深度优化的DOM结构遍历,也就是说,移动的方向至少有两个(取决于使用的遍历类型)。遍历以给定节点为根,不可能向上超出DOM树的根节点。


    DOM树

    如上图的DOM树,从document依序向前。从文档最后的文本节点开始,遍历可以反向移动到DOM树的顶端。NodeIterator 和 TreeWalker 都以这种方式执行遍历。

    3.1、NodeIterator

    NodeIterator类型是两者中比较简单的第一个,可以使用document,createNodeIterator() 方法创建它的实例。这个方法接受以下4个参数:

    • root:想要作为搜索起点的节点
    • whatToShow:表示要访问哪些节点的数字代码。
    • filter:是一个NodeFilter 对象,或者一个表示应该接受还是拒绝某种特定节点的函数。
    • entityReferenceExpansion:布尔值,表示是否要扩展实体引用。这个参数在HTML页面中没有用,因为其中的实体引用不能扩展。

    whatToShow参数是一个位掩码,通过应该一或多个过滤器(filter) 来确定要访问哪些节点。这个参数的值以常量形式在 NodeFilter 类型中定义,如下(忽略了对HTML没用的常量):

    • NodeFilter.SHOW_ALL: 显示所有类型的节点。
    • NodeFIlter.SHOW_ELEMENT:显示元素节点。
    • NodeFilter.SHOW_ATTRIBUTE:显示特性节点。由于DOM结构原因,实际上不能使用这个值。
    • NodeFilter.SHOW_TEXT:显示文本节点。
    • NodeFilter.SHOW_COMMENT:显示文本节点
    • NodeFilter.SHOW_DOCUMENT:显示文档节点
    • NodeFilter.SBOW_DOCUMENT_TYPE:显示文档类型节点

    filter参数指定自定义的NodeFilter对象,或者指定一个功能类似节点过滤器(node filter)的函数。每个NodeFilter对象只有一个方法,即accept-Node()

    • 如果应该访问给定的节点,该方法返回 NodeFilter.FILTER_ACCEPT;
    • 如果不应该访问给定的节点,该方法返回NodeFilter.FILTER_SKIP

    由于NodeFilter是一个抽象的类型,因此不能直接创建它的实例。在必要时,只要创建一个包含 acceptNode() 方法对象,然后将这个对象传入 createNodeIterator() 中即可
    创建一个 值显示 <p> 元素的节点迭代器

    var filter = {
      acceptNode: function(node) {
        return node.tagName.toLowerCase() == 'p' ? 
        NodeFilter.FILTER_ACCEPT :
        NodeFilter.FILTER_SKIP
      }
    }
      
    var iterator = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, filter, false)
    

    第三个参数也可以是一个与acceptNode() 方法类似的函数

    var filter = function() {
      return node.tagName.toLowerCase() == 'p' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
    }
    

    一般来说,后者比较常用。如果不知道过滤器,那么应该把第三个参数设置为 null

    NodeIterator 类型的两个主要方法 nextNode()、previousNode(),分别表示向前一步和向后一步。
    在刚刚创建的 NodeIterator 对象中,有一个内部指针指向根节点,因此第一个调用 nextNode() 会返回根节点。当遍历到DOM子数的最后一个节点时,nextNode() 返回 null
    。previousNode()的工作机制类似,在 previousNode()返回根节点之后,再次调用返回null

    <div id="div">
      <p><b>Hello World</b></p>
      <ul>
        <li>List Item 1</li>
        <li>List Item 2</li>
        <li>List Item 3</li>
      </ul>
    </div>
    

    遍历如上结构

    var div = document.getElementById('div')
    var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, null, false)
    
    var node = iterator.nextNode()
    while (node != null) {
      console.log(node.tagName)
      node = iterator.nextNode()
    }
    /*
    DIV
    P
    B
    UL
    LI
    LI
    LI
    */
    

    如果只想返回某一个元素,比如LI,可以为他定义一个过滤器:

    var filter = function(node) {
      return node.tagName.toLowerCase() == 'li' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
    }
    
    var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, filter , false)
    
    // todo
    

    由于 nextNode() 和 previousNode() 方法都基于 NodeIterator 在 DOM 结构中的内部指针工作,所以DOM结构的变化会反映在遍历的结果中。

    3.2、TreeWalker

    TreeWalker 时 NodeIterator 的一个更高级版本。除了包括 nextNode() 和 previousNode() 在内的相同功能之外,还提供了下列的方法:

    • parentNode():遍历到当前节点的父节点
    • firstChild():遍历到当前节点的第一个子节点
    • lastChild():遍历到当前节点的最后一个子节点
    • nextSibling():遍历到当前节点的下一个同辈节点
    • previousSibling():遍历到当前节点的上一个同辈节点。

    创建 TreeWalker 对象要使用 document.createTreeWalker() 方法,这个方法接受4个参数(与 createNodeIterator()相同 ):

    • 作为遍历起点的根节点
    • 要显示的节点类型
    • 过滤器
    • 表示是否扩展实体引用的布尔值
    var div = document.getElementById('div')
    
    var filter = function(node) {
      return node.tagName.toLowerCase() == 'li' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
    }
    
    var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, filter, false)
    var node =walker.nextNode()
    
    while (node != null) {
      console.log(node.tagName)
      node = walker.nextNode()
    }
    

    由于这两个创建方法很相似,所以很容易用 TreeWalker 来代替 NodeIterator:

    TreeWalker 真正强大的地方在于能够在DOM结构中沿任何方向移动。使用TreeWalker 遍历DOM树,即使不定义过滤器,也可以取得所有的 <li> 元素。

    var div = document.getElementById('div')
    var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, null, false)
    
    walker.firstChild() // 转到 <p>
    walker.nextSibling() // 转到 <ul>
    
    var node = walker.firstChild(); // 第一个<li>
    while(node !== null) {
      console.log(node.tagName)
      node = nextSibling() // 同级<li>
    }
    

    TreeWalker 类型还有一个属性,名叫 currentNode,表示任何遍历方法在 上一次遍历中返回的节点。通过设置这个属性也可以修改继续进行的起点

    var node =walker.nextNode()
    console.log(node == walker.currentNode) // true
    walker.currentNode = document.body // 修改起点
    

    四、范围

    "DOM2级遍历和范围"模块定义了“范围”(range)接口。通过范围可以选择文档中的一个区域,而不必考虑节点的界限。

    4.1、DOM中的范围

    DOM2级在 Document 类型中定义了 createRange() 方法。使用 hasFeature() 或者直接检测该方法,都可以确定浏览器是否支持范围。

    var supportsRange = document.implementation.hasFeature('Range', '2.0')
    var alsoSupportsRange = (typeof document.createRange == 'function')
    

    创建范围通过 createRange()方法

    var range = document.createRange()
    

    新创建的范围也直接与创建它的文档关联在一起,不能用于其他文档。
    每一个范围由一个Range 实例表示,拥有以下属性和方法:

    • startContainer:包含范围起点的节点(即选区中第一个节点的父节点)
    • startOffset:范围在 startContainer 中起点的偏移量。如果 startContainer 是文本节点、注释节点、CDATA节点,那么startOffset 就是范围起点之前跳过的字符数量,否则, startOffset 就是范围中第一个子节点的索引
    • endContainer:包含范围终点的节点(即选区中第一个节点的父节点)
    • endOffset:范围在 endContainer 中终点的偏移量(与startOffset 遵循相同的取值规则)
    • commonAncestorContainer:startContainer 和 endContainer 共同的祖先节点咋爱文档树中位置最深的那个

    在把范围放到文档中特定的位置时,这些属性都会被赋值。

    4.1.1、用DOM范围实现简单选择

    要使用范围来选择文档中的一部分,最简单的方式就是 使用 selectNode()selectNodeContents()。这两个方法都接受一个参数:即一个DOM节点,然后使用该节点中的信息来填充范围。selectNode() 方法选择整个节点,包括器子节点; 而 selectNodeContents() 方法则只选择节点的 子节点。

    <body>
      <p id="p1"><b>Hello</b> World</p>
    </body>
    

    根据如上代码来创建文档

    var rang1 = document.createRange()
    var rang2 = document.createRange()
    var p1 = document.getElementById('p1')
    
    rang1.select(p1) // 包含 p 元素
    rang2.selectNodeContents(p1) // 不包含 p 元素
    
    示例图

    在调用selectNode() 时,startContainer、endContainer、commonAncestorContainer都等于 传入节点的父节点,而 startOffset 属性等于 给定节点在其父节点的 childNodes 集合中的索引(在这个例子中是1——因为兼容DOM的浏览器会将空格算作第一个文本节点),endOffset 等于 startOffset 加 1(因为只选择了一个节点)

    在掉头 selectNodeContents() 时,startContainer、endContainer 和 commonAncestorContainer 等于传入的节点,而startOffset 属性始终等于0,因为范围从给定节点的第一个子节点开始。endOffset 等于子节点的数量,在这个例子中为2

    为了 更精确地控制将哪些节点包含在范围中,还可以使用下列方法

    • setStartBefore(refNode):将范围的起点设置在 refNode 之前,因此 refNode 也就是范围选区中的第一个子节点。同时会将 startContainer 属性设置为 refNode.parentNode,将startOffset 属性设置为 refNode 在其父节点的 childNodes 集合中的索引。
    • setStartAfter(refNode):将范围的起点设置在 refNode 之后,因此 refNode也就不再范围之内了,其下一个同辈节点才是范围选区中的第一个子节点。同时会将 startContainer 属性设置为 refNode.parentNode, 将 startOffset 属性设置为 refNode 在其父节点的 childNodes 集合中的索引 加 1
    • setEndBefore(refNode):将范围的终点设置在 refNode 之前,因此 refNode 也就不在范围之内了,其上一个同辈节点才是范围选区中的最后一个子节点。同时会将 endContainer 属性设置为 refNode.parentNode 将 endOffset 属性设置为 refNode 在其父节点的 childNodes 集合中的索引。
    • setEndAfter(refNode):将范围的终点设置在 refNode 之后,因此 refNode 也就是 范围选区中的最后一个子节点。同时会将 endContainer 属性设置为 refNode.parentNode,将endOffset 属性设置为 refNode 在其父节点的 childNodes 集合中的索引 加 1。

    调用这些方法的时候,所有属性都会自动为你设置号。要想创建复杂的范围选区,也可以直接指定这些属性的值。

    4.1.2、用DOM 范围实现复杂选择

    要创建复杂的范围就得使用 setStart()setEnd() 方法。这两个方法都接受两个参数:参照节点、偏移量。可以使用这两个方法来模仿 selectNode() 和 selectNodeContents()

    var range1 = document.createRange()
    var range2 = document.createRange()
    var p1 = document.getElementById('p1')
    var p1Index = -1
    
    for (var i = 0, len = p1.parentNode.childNodes.length; i < len; i++) {
      if (p1.parentNode.childNodes[i] == p1) {
        p1Index = i
        break
      }
    }
    
    range1.setStart(p1.parentNode, p1Index)
    range1.setEnd(p1.parentNode, p1Index + 1)
    range2.setStart(p1, 0)
    range2.setStart(p1, p1.childNodes.length)
    

    模仿selectNode() 和 selectNodeContents() 并不是 setStart() 和 setEnd() 的主要用途,它们更胜一筹的地方在于能够选择节点的一部分。

    假设你只想选择前面 HTML 实例代码从 "Hello" 的 "llo" 到 "world" 的 "o" ——很容易就能做到

    // 获取所有节点的引用
    var p1 = document.getElementById('p1')
    var helloNode = p1.firstChild.firstChild
    var worldNode = p1.lastChild
    
    // 创建范围 指定起点和终点
    var range = document.createRange()
    range.setStart(helloNode, 2) // 设置偏移两个字符
    range.setEnd(worldNode, 3) // 偏移三个字符(前面的空格)
    
    图解

    当然,仅仅是选择了文档中的某一部分用处并不是很大。但重要的是,选择之后才可以对选区进行操作

    4.1.3、操作DOM 范围中的内容

    在创建范围时,内部会为这个范围创建一个文档片段,范围所属的全部节点都被添加到了这个文档片段中。为了创建这个文档片段,范围内容的格式必须正确有效。
    对于上年截取字符的代码,范围经过计算知道选区缺少了一个开始的<b>标签,就会在后台动态加入一个该标签,同时还会在前面加入一个表示结束的</b>标签以结束“He”。并且重构一个有效良好的DOM格式

    llo</b> wo ====> <p><b>He</b><b>llo</b> world!</p>
    

    在创建了范围之后,就可以使用各种方法对范围的内容进行操作了(注意,表示范围的内部文档片段中的所有节点,都只是指向文档中相应节点的指针)

    deleteContents()
    这个方法能够从文档中删除范围所包含的内容

    var range = document.createRange()
    range.setStart(helloNode, 2)
    range.setEnd(worldNode, 3)
    range.deleteContents()
    

    此时页面会显示如下HTML代码

    <p id="p1"><b>He</b>rld</p>
    
    图解
    由于范围选区在修改底层DOM 结构时能够保证格式良好,因此即使内容被删除了,最终的 DOM 结构依旧是 格式良好的。

    extractContents() 也会从文档中移除范围选区。区别在于:extractContents() 会返回范围的文档片段。利用这个返回的值,可以将范围的内容插入到文档中的其他地方。

    var fragment = range.extractContents()
    p1.parentNode.appendChild(fragment)
    

    此时的文档结构

    <p id="p1"><b>He</b>rld</p>
    <b>llo</b> Wo
    

    还有一种做法,使用 cloneContents() 创建范围对象的一个副本,然后在文档的其他地方插入该副本

    var fragment = range.cloneContents()
    p1.parentNode.appendChild(fragment)
    

    此时的文档结构

    <p id="p1"><b>Hello</b> World</p>
    <b>llo</b> Wo
    

    需要注意的是:在调用上面介绍的方法之前,拆分的节点并不会产生格式良好的文档片段。换句话说,原始的HTML在DOM被修改之前会始终保持不变。

    4.1.4、插入DOM范围中的内容

    insertNode() 方法可以想范围选区的开始插入一个节点。
    假如在前面示例的 HTML 代码中插入 <span style="color: red"> Insterted text <span>

    var p1 = document.getElementById('p1')
    var helloNode = p1.firstChild.firstChild
    var worldNode = p1.lastChild
    var range = document.createRange()
    
    var span = document.createElement('span')
    span.style.color = 'red'
    span.appendChild(document.createTextNode('Insterted text'))
    range.insertNode(span)
    

    此时的文档结构

    <p id="p1"><b>He<span style="color: red;">Insterted text</span>llo</b> World</p>
    

    除了向范围内部 插入内容之外,还可以围绕范围插入内容,surroundContents() 方法。接受一个参数,即环绕范围内容的节点。在环绕范围插入内容时,后台会执行下列步骤。

    1. 提取除范围中的内容(类是执行 extractContent())
    2. 将给定节点插入到文档中原来范围所在的位置上;
    3. 将文档片段的内容添加到给定节点中。
    
    range.selectNode(helloNode) // <b>Hello</b>
    
    var span = document.createElement("span")
    span.style.backgroundColor = 'yellow'
    range.surroundContents(span)
    

    此时的文档变化

    <b><span style="background-color: yellow;">Hello</span></b>
    

    为了插入<span>,范围必须包含整个 DOM 选区( 不能仅仅包含选中的 DOM 节点 )

    4.1.5、折叠DOM范围

    所谓 折叠范围,就是指范围中未选择文档的任何部分。

    collapse()方法来折叠范围,这个方法接受一个参数,一个布尔值,表示要折叠到范围的哪一端。参数 true 表示折叠到范围的起点,false表示折叠到范围的终点。要确定范围异界折叠完毕,可以检测 collapsed 属性。

    range.collapse(true) // 折叠到起点
    console.log(range.collapsed)
    

    某个范围是否处于折叠状态,可以帮我嫩确定范围中的两个节点是否紧密相邻。

    <p id="p1"> Paragraph 1</p><p id="p2">Paragraph 2</p>
    

    如上结构:如果我们不知道其构成(比如说,这是动态生成的代码),那么可以像下面这样创建一个范围。

    var p1 = document.getElementById('p1')
    var p2 = document.getElementById('p2')
    var range = document.createRange()
    
    range.setStartAfter(p1)
    range.setEndBefore(p2)
    console.log(range.collapsed) // ,true
    

    这个例子中,新创建的范围时折叠的 因为p1 的后面 和 p2的前面什么也没有。

    4.1.6、比较DOM范围

    在有多个DOM范围的情况下,可以使用 compareBoundaryPoints()来确定这些范围是否有公共的边界(起点或终点)。接受两个参数:表示比较方式的常量值、要比较的范围。
    表示比较的常量值:

    • Range.START_TO_START(0):比较第一个范围和第二个范围的起点
    • Range.START_TO_END(1):比较第一个范围的起点和第二个范围的终点
    • Range.END_TO_END(2):比较第一个范围和第二个范围的终点
    • Range.END_TO_START(3):比较第一个范围的终点和第一个范围的起点。

    compareBoundaryPoints()方法可能的返回值如下:

    • 如果第一个范围中的点位于第二个范围中的点之后,返回 -1
    • 如果两个点相等,返回0
    • 如果第一个范围中的点位于第二个范围中的点之后,返回1。
    <p id="p1"><b>Hello</b> World!</p>
    
    var range1 = document.createRange()
    var range2 = document.createRange()
    var p1 = document.getElementById('p1')
    
    range1.selectNodeContents(p1)
    range2.selectnodeContents(p1)
    range2.setEndBefore(p1.lastChild)
    
    console.log(range1.compareBoundaryPoints(Range.START_TO_START, range2)) // 0
    console.log(range1.compareBoundaryPoints(Range.END_TO_END, range2)) // 1
    
    图示
    4.1.7、复制 DOM 范围

    cloneRange() 方法 可以复制范围。这个方法会创建调用它的范围的一个副本

    var newRange = range.cloneRange()
    

    要创建的范围于原来的范围包含相同的属性,而修改它的端点不会影响到原来的副本

    4.1.8、清理DOM 范围

    在使用完范围之后,最好是调用 detach() 方法,以便从创建范围的文档中分离出该范围。调用detach()之后,就可以方法的解除对范围的引用,从而让垃圾回收机制回收其内存了。

    range.detach() // 从文档中分离
    range = null // 解除引用
    

    4.2、IE8及更早版本中的范围

    虽然IE9支持DOM范围,但IE8及之前版本不支持DOM范围。IE8及早期版本支持一种类似的概念,即文本范围(text range)。文本范围是IE专有的特性,其他浏览器都不自持。
    通过<body>、<button>、<input>、<textarea>等这几个元素,可以调用 createTextRange() 方法来创建文本范围

    var range = document.body.createTextRange()
    
    4.2.1、用IE范围实现简单的选择

    findText()
    选择页面中某一区域的最简单方式,就是使用范围的findText()方法。这个方法会找到第一次出现的给定文本,并将范围移过来以环绕该文本。如果没有找到文本,这个反法返回false;否则返回 true。
    以如下html 为例

    <p id="p1"><b>Hello</b> World!</p>
    
    var range = document.body.createTextRange()
    var found = range.findText("Hello")
    console.log(found) // true——代表找到文本
    console.log(found.text) // Hello
    

    还可以为 findText() 传入另一个参数,表示向哪个方向继续搜索的数值。负值表示应该从当前位置向后搜索,而正值表示应该从当前位置向前搜索。

    var found = range.findText('Hello')
    var foundAgain = range.findText('Hello', 1)
    

    IE中与DOM中的 selectNode() 方法最接近的方法是 moveToElementText(),这个方法接受一个 DOM 元素,并选择改元素的所有文本,包括HTML标签。

    var range = document.body.createTextRange()
    var p1 = document.getElementById('p1')
    range.moveToElementText(p1)
    

    在文本范围中包含HTML的情况下,可以使用 htmlText 属性取得范围的全部内容,包括 HTML 和文本。

    console.log(range.htmlText) //  <P id=p1><B>Hello</B> World!</P>
    

    IE的范围没有任何属性可以随着范围选区的变化而动态更新。不过,其 parentElement() 方法倒是与 DOM 的 commonAncestorContainer 属性类似

    var ancestor = range.parentElement()
    

    这样得到的父元素 始终都可以反映文本选区的父节点

    4.2.2、使用IE范围实现复杂的选择

    在IE中创建范围的方法,就是以特定的增向量向四周移动范围。IE提供了4个方法:move()、moveStart()、moveEnd() 和 expand()。这些方法都接受两个参数:移动单位、移动单位的数量
    移动单位是下列一种字符串值

    • character:逐个字符地移动
    • word:逐个单词(一系列非空格字符)地移动
    • sentence:逐个句子(一些列以句号、问好、叹号结尾的字符)地移动
    • textedit:移动到当前范围选区的开始或接受位置

    通过 moveStart()方法可以移动范围的起点,通过moveEnd()可以移动范围的终点,移动的幅度由单位数量指定

    range.moveStart('word', 2) // 移动两个单词
    range.moveEnd('character', 1) // 移动一个字符
    

    使用expand()方法可以将范围规范化。expand() 方法的作用是将任何部分选择的文本全部选中。例如,当前选择的是一个单词中间的两个字符,调用expand("word")可以将整个单词都包含在范围之内。


    move()方法则首先会折叠当前范围(让起点和终点相等),然后再将范围移动指定的单位数量。

    range.move('character', 5)
    

    调用 move() 之后,范围的起点和终点相同,因此必须再使用 moveStart() 或 moveEnd() 创建新的选区

    4.2.3、操作IE范围中的内容

    使用 text属性 或 pasteHTML() 方法。通过 text 属性可以取得范围中的内容文本;但是,也可以通过这个属性设置范围中的文本内容

    var range = document.body.createTextRange()
    range.findText('Hello')
    range.text = 'Howdy'
    

    此时的文档结构

    <P id=p1><B>Howdy</B> World!</P>
    

    要向 范围中插入 HTML代码,就得使用 pasterHTML() 方法,

    var range = document.body.createTextRange()
    range.findText('Hello')
    
    range.pasteHTML('<em>Howdy</em>')
    

    此时的文档结构

    <P id=p1><B><EM>Howdy</EM></B> World!</P>
    

    不过,在范围中包含HTML代码是,不应该使用pasteHTML(),因为这样很可能会导致不可预料的结果——很可能是格式不对的HTML

    4.2.4、折叠IE范围

    IE为范围提供的 collapse() 方法与相应的 DOM 方法用法一样:传入 true 把范围折叠到起点,传入false把范围折叠到终点

    range.collapse(true) // 折叠到起点
    

    可惜的是,没有对应的 collapsed 属性让我们知道范围是否折叠完毕。为此,需要使用到 boundingWidth 属性,该属性返回范围的宽度(以像素为单位)。如果 boundingWidth 属性等于 0,就说明范围已经折叠了。

    var isCollapsed = (range.boundingWidth == 0)
    

    此外还有 boundingHeight、boundingLeft、boundingTop 等属性,虽然他们都不像 boundingWidth 那么有用,但也可以提供一些有关范围位置的信息。

    4.2.5、比较IE范围

    IE中的 compareEndPoints() 方法与 DOM 范围的 compareBoundaryPoints() 方法类似。接受两个参数:比较的类型、比较的范围。
    比较类型与DOM相似

    • StartToStart
    • StartToEnd
    • EndToEnd
    • EndToStart

    与DOM类似,返回值有三种情况

    • 第一个范围边界位于第二个范围边界前面,返回 -1
    • 二者边界相同,返回 0
    • 如果第一个范围的边界位于第二个范围的边界后面,返回 1
    var range1 = document.body.createTextRange()
    var range2 = document.body.createTextRange()
    
    range1.findText('Hello World')
    range2.findText('Hello')
    
    console.log(range1.compareEndPoints('StartToStart', range2))  // 0
    console.log(range1.compareEndPoints('EndToEnd', range2)) // 1
    

    IE中 还有两个方法,也是用于比较范围的:isEqual() 用于确定两个范围是否相等,inRange() 用于确定一个范围是否包含另一个范围。

    var range1 = document.body.createTextRange()
    var range2 = document.body.createTextRange()
    
    range1.findText('Hello World')
    range2.findText('Hello')
    
    console.log(range1.isEqual(range2)) // false
    console.log(range1.inRange(range2)) // true
    
    4.5.6、复制IE范围

    在IE中使用 duplicate() 方法可以复制文本范围,结果会创建原范围的一个副本

    var newRange = range.duplicate()
    

    新创建的范围会带有与原范围完全相同的属性

    五、小结

    "DOM2 级样式" 模块只要针对操作元素的样式信息而开发,其特征简要总结如下。

    • 每个元素都有一个管理的 style 对象,可以用来确定和修改行内的样式。
    • 要确定某个元素的计算样式(包括应用给它的所有CSS规则),可以使用 getComputedStyle() 方法。
    • IE不支持 getComputedStyle() 方法,但为所有元素都提供了能够返回相同信息的 currentStyle属性。
    • 可以通过 document.styleSheets 集合访问样式表
    • 除IE之外的所有浏览器都支持对样式表的接口,IE也为几乎所有相应的DOM功能提供了自己的一套属性和方法
      "DMO2级遍历和范围"模块提供了与DOM结果交互的不同方式,简要总结如下:
    • 遍历即使用 NodeIteratorTreeWalker 对 DOM执行深度优化的遍历
    • NodeIterator 是一个简单的接口,只允许一个节点的步幅前后移动。而 TreeWalker 在提供相同功能的同时,还支持在DOM结构的各个方向上移动,包括父节点、同辈节点、子节点等。
    • 范围是选择 DOM 结构中特定部分,然后再执行相应操作的一种手段
    • 使用范围选区可以在删除文档中某些部分的同时,保存文档结构的良好,或者复制文档中的相应部分
    • IE8及更早版本不支持"DOM2级遍历和范围" 模块,但它提供了一个专有的文本范围对象,可以用来完成简单的基于文本的范围操作。IE9完全支持DOM遍历。

    相关文章

      网友评论

          本文标题:DOM2 和 DOM3

          本文链接:https://www.haomeiwen.com/subject/laofdctx.html