5.2 DOM 树操作

刚才介绍的.attr().prop()方法都是在修改文档时的得力工具。但我们还没有涉及怎样修改DOM文档的结构。要想操作DOM树本身,需要再深入了解一下jQuery库的核心函数。

5.2.1 重新认识$()函数

从本书开始到现在,我们一直在使用$()函数来访问文档中的元素。这个函数就像一个工厂,它能够生成一个jQuery对象,指向CSS选择符所描述的一组元素。

然而,除了选择元素之外,$()函数的圆括号内还有另外一个玄机——这个强大的特性使得$()函数不仅能够改变页面的视觉外观,更能改变页面中实际的内容。只要在这对圆括号中放入一组HTML元素,就能轻而易举地改变整个DOM结构。

 关于可访问性的提示

再次重申,无论什么时候都不应该忘记,我们添加的所有功能、视觉效果或者文本性的信息,只有在可以使用(并启用了)JavaScript的Web浏览器中才能正常有效。但是,重要的信息应该对所有人都是可以访问的,而不应该只针对使用了正确的软件的人。

5.2.2 创建新元素

在FAQ页面中,一个常见的功能是出现在每一对“问题—答案”后面的back to top(返回页面顶部)链接。通常,这些链接并没有语义上的价值,因而可以合理地通过JavaScript来生成它们,将它们作为访问者所浏览页面的一个增强的子功能。在我们的例子中,需要为每个段落后面添加一个back to top链接,而且,也需要添加作为back to top链接返回目标的锚。首先,我们来创建新元素,参见代码清单5-6。

代码清单5-6

  1. //未完成的代码
  2. $(document).ready(function() {
  3. $('<a href="#top">back to top</a>');
  4. $('<a id="top"></a>');
  5. });

第一行代码中创建了back to top链接,而第二行代码则为这个链接创建了一个作为目标的锚。但是,页面中还没有出现back to top链接。图5-3是此时页面的外观。

5.2 DOM 树操作 - 图1

图 5-3

虽然前面的两行代码创建了新的元素,但是还没有把它们添加页面中。为此,我们可以选择 使用jQuery提供的众多插入方法中的一种。

5.2.3 插入新元素

jQuery提供了很多将元素插入到文档中的方法。每一种方法的名字都表明了新内容与已有内容之间的关系。例如,我们想把 back to top 链接插入到每个段落后面,因此就可以使用.insertAfter()方法,参见代码清单5-7。

代码清单5-7

  1. //未完成的代码
  2. $(document).ready(function() {
  3. $('<a href="#top">back to top</a>').insertAfter('div.chapter p');
  4. $('<a id="top"></a>');
  5. });

在将链接实际地插入到页面(也插入到DOM)中之后,<div class="chapter">中的每个段落后面,都应该出现back to top链接,如图5-4所示。

5.2 DOM 树操作 - 图2

图 5-4

我们注意到,新链接出现在单独的一行中,并没有出现在段落内部。这是因为insertAfter()方法及其对应的.insertBefore()方法,都是在指定的元素外部插入新内容。

不过,现在的链接还不能用。因此,我们需要再插入id="top"的锚。要插入这个锚,可以选用一种在其他元素中插入元素的方法,参见代码清单5-8。

代码清单5-8

  1. $(document).ready(function() {
  2. $('<a href="#top">back to top</a>').insertAfter('div.chapter p');
  3. $('<a id="top"></a>').prependTo('body');
  4. });

新增的代码在<body>的开头,也就是页面的顶部插入了锚元素。在通过.insertAfter()方法插入链接和.prependTo()方法插入锚之后,这个页面就具备了完备的back to top链接。

如果再算上.appendTo()方法,那我们就已经知道了在其他元素前、后插入新内容的一套方案。

  • .insertBefore()在现有元素外部、之前添加内容;

  • .prependTo()在现有元素内部、之前添加内容;

  • .appendTo()在现有元素内部、之后添加内容;

  • .insertAfter()在现有元素外部、之后添加内容。

5.2.4 移动元素

在back to top链接的例子中,我们创建了新元素并把它们插入到了页面上。此外,也可以取得页面中某个位置上的元素,将它们插入到另一个位置上。动态地放置并格式化脚注,就是这种插入操作在实际中的一种应用。现在,Flatland的原始文本中已经包含了一个这样的脚注,但为了示范这种应用,下面我们还需要将文本其他几个部分指定为脚注:

  1. <p>How admirable is the Law of Compensation! <span
  2. class="footnote">And how perfect a proof of the natural
  3. fitness and, I may almost say, the divine origin of the
  4. aristocratic constitution of the States of Flatland!</span>
  5. By a judicious use of this Law of Nature, the Polygons and
  6. Circles are almost always able to stifle sedition in its
  7. very cradle, taking advantage of the irrepressible and
  8. boundless hopefulness of the human mind.&hellip;
  9. </p>

这个HTML文档中包含三个脚注,上面这个段落里包含一个。脚注的文本包含在段落的文本中,通过<span class="footnote"></span>隔开。通过以这种方式来标记HTML,能够保持脚注在上下文中的关系。在为脚注应用了斜体样式规则后,这个段落的外观如图5-5所示。

5.2 DOM 树操作 - 图3

图 5-5

接下来,需要提取出这些脚注,然后把它们插入到文档的底部,具体来说,就是插入到<div class="chapter"><div id = "footer">之间。

不过,这里我们要记住的一点是,即使是在隐式迭代的情况下,插入的顺序也是预定义的,即从DOM树的上方开始向下依次插入。由于维持脚注在页面上新位置中的顺序很重要,所以我们应该使用.insertBefore('#footer')。这样,footnote 1会被放在<div class= "chapter"><div id="footer">之间,footnote 2会被放在footnote 1和<div id="footer">之间,然后依此类推。但是,如果在这里使用.insertAfter('div.chapter'),那么脚注的次序就会颠倒。

因此,当前的代码应该如代码清单5-9所示。

代码清单5-9

  1. $(document).ready(function() {
  2. $('span.footnote').insertBefore('#footer');
  3. });

由于脚注放在<span>标签中,这就意味着它们在默认情况下应该显示为行内盒子,因此会导致这3个脚注前后相连,从视觉上无法将它们区分开来。不过,我们已经使用CSS解决了这个问题,即将处于<div class="chapter">外部的span.footnote元素的display属性设置为block

这样,我们的脚注就具备了雏形,如图5-6所示。

5.2 DOM 树操作 - 图4

图 5-6

至少,它们现在可以从视觉上明显地分开。然而,围绕这些脚注还有很多后续工作要做。更加健壮的一种脚注方案应该:

  • 为每个标注编号;

  • 在正文中标出提取脚注的位置,使用脚注的编号;

  • 在文本中的位置上创建一个指向对应脚注的链接,在脚注中创建返回文本位置的链接。

5.2.5 包装元素

脚注的编号可以直接在标记中添加,但在这里我们要利用标准的有序列表来生成序号。为此,需要先创建一个用于包装所有脚注的<ol>元素,并为每个脚注分别创建一个<li>元素。这时候就要用到包装方法了。

要在一个元素中包装另一个元素,必须知道是把每个元素分别包装在各自的容器中,还是把所有元素包装在一个容器中。考虑到要为每个脚注编号,我们需要实现这两种形式的包装,参见代码清单5-10。

代码清单5-10

  1. $(document).ready(function() {
  2. $('span.footnote')
  3. .insertBefore('#footer')
  4. .wrapAll('<ol id="notes"></ol>')
  5. .wrap('<li></li>');
  6. });

把脚注插入到页脚前面后,我们使用.wrapAll()把所有脚注都包含在一个<ol>中。然后再使用.wrap()将每一个脚注分别包装在自己的<li>中。从图5-7可以看出,这样就为脚注添加了正确的编号。

5.2 DOM 树操作 - 图5

图 5-7

接下来,我们要考虑为提取脚注的位置加标记和编号了。为了简单起见,我们这次需要重写现有的代码,不再依赖隐式迭代。

显式迭代

我们知道,.each()方法就是一个显式迭代器,与最近加入JavaScript语言中的数组迭代器forEach()非常相似。如果在使用隐式迭代的情况下,我们想为每个匹配的元素应用的代码显得太过复杂,就可以转而使用.each()。这个方法接受一个回调函数,这个函数会针对匹配的元素集中的每个元素都调用一次,如代码清单5-11所示。

代码清单5-11

  1. $(document).ready(function() {
  2. var $notes = $('<ol id="notes"></ol>').insertBefore('#footer');
  3. $('span.footnote').each(function(index) {
  4. $(this).appendTo($notes).wrap('<li></li>');
  5. });
  6. });

这样修改的动机稍后大家就会明白。首先,需要理解传递给.each()回调的信息。

与其他回调函数(比如本章前面介绍的值回调函数)类似,在回调函数中,this关键字指向当前正在操作的DOM元素,在代码清单5-11中,我们使用这个上下文创建了指向脚注<span>的jQuery对象,将它添加到idnotes<ol>中,最后把它封装在<li>元素里。

为了在正文中标记提取脚注的位置,可以利用.each()回调的参数。这个参数表示迭代的次数,从0开始,每迭代一次就加1。因此这个数值始终都比当前的脚注编号小1。可以利用这个参数在正文中生成适当的标签,如代码清单5-12所示。

代码清单5-12

  1. $(document).ready(function() {
  2. var $notes = $('<ol id="notes"></ol>').insertBefore('#footer');
  3. $('span.footnote').each(function(index) {
  4. $('<sup>' + (index + 1) + '</sup>').insertBefore(this);
  5. $(this).appendTo($notes).wrap('<li></li>');
  6. });
  7. });

这样,在脚注被从正文中提取出来并插入到页面底部之前,我们创建了一个包含脚注编号的<sup>元素,并将它插入到正文中。这里的操作顺序十分重要。必须要在脚注被移动之前插入这个编码,否则就找不到原始位置了。另外,还要注意表达式index+1必须放在括号中,这样才表示是一个加法运算,因为“+”在JavaScript中也可以用于拼接字符串。

这时候再看看页面(参见图5-8),其中原来的脚注位置就出现了相应的编号。

5.2 DOM 树操作 - 图6

图 5-8

5.2.6 使用反向插入方法

在代码清单5-12中,我们先把创建的内容插入到元素前面,然后再把同一个元素插入到文档中的另一个位置。通常,当在jQuery中操作元素时,利用连缀方法更简洁也更有效。可是我们现在没有办法这样做,因为this.insertBefore()的目标,是.appendTo()的内容。此时,利用反向插入方法,可以帮我们解决问题。

.insertBefore().appendTo()这样的插入方法,一般都有一个对应的反向方法。反向方法也执行相同的操作,只不过“目标”和“内容”正好相反。例如:

  1. $('<p>Hello</p>').appendTo('#container');

与下面的代码结果一样:

  1. $('#container').append('<p>Hello</p>');

下面我们就使用.before()代替.insertBefore()来重构代码,参见代码清单5-13。

代码清单5-13

  1. $(document).ready(function() {
  2. var $notes = $('<ol id="notes"></ol>')
  3. .insertBefore('#footer');
  4. $('span.footnote').each(function(index) {
  5. $(this)
  6. .before('<sup>' + (index + 1) + '</sup>')
  7. .appendTo($notes)
  8. .wrap('<li></li>');
  9. });
  10. });

 插入方法回调

反向插入方法可以接受一个函数作为参数,与.attr().css()方法类似。这个传入的函数会针对每个目标元素调用,返回被插入的HTML字符串。在此其实也可以使用这个技术,但由于这样就需要对每个脚注都重复一遍相同的操作,所以还是使用一个.each()方法来得更清晰。

现在,我们可以考虑最后一步了:在正文中相应的位置创建指向匹配脚注的链接和在脚注中创建指向正文位置的链接。为此,每个脚注需要4处标记:两个链接,一个在正文中,一个在脚注中;以及两个id属性。因为这样一来,传入.before()方法的参数会变得复杂,所以有必要在这里使用一种新的创建字符串的方法。

在代码清单5-13中,我们使用了“+”操作符来拼接字符串。使用+操作符虽然没有问题,但如果要拼接的字符串太多,那看起来就会很乱。所以,我们在这里使用数组的.join()方法来构建一个更大的数组。换句话说,下面的两行代码结果相同。

  1. var str = 'a' + 'b' + 'c';
  2. var str = ['a', 'b', 'c'].join('');

虽然这个例子要求输入更多字符,但使用.join()方法可以避免因要拼接的字符串过多而引起混乱。下面我们再看看示例代码吧,代码清单5-14就是使用.join()创建字符串的过程。

代码清单5-14

  1. $(document).ready(function() {
  2. var $notes = $('<ol id="notes"></ol>')
  3. .insertBefore('#footer');
  4. $('span.footnote').each(function(index) {
  5. $(this)
  6. .before([
  7. '<sup>',
  8. index + 1,
  9. '</sup>'
  10. ].join(''))
  11. .appendTo($notes)
  12. .wrap('<li></li>');
  13. });
  14. });

注意,由于数组的每个元素会分别执行运算,因此不再需要把index+1放在括号里了。

使用这种技巧,可以为脚注标签添加一个指向页面底部的链接和一个唯一的id值。同时在后面的方法中,也要给<li>元素中添加相应的id属性,以便该链接有匹配的目标,参见代码清单5-15。

代码清单5-15

  1. $(document).ready(function() {
  2. var $notes = $('<ol id="notes"></ol>')
  3. .insertBefore('#footer');
  4. $('span.footnote').each(function(index) {
  5. $(this)
  6. .before([
  7. '<a href="#footnote-',
  8. index + 1,
  9. '" id="context-',
  10. index + 1,
  11. '" class="context">',
  12. '<sup>',
  13. index + 1,
  14. '</sup></a>'
  15. ].join(''))
  16. .appendTo($notes)
  17. .wrap('<li id="footnote-' + (index + 1) + '"></li>');
  18. });
  19. });

添加了这些标记之后,每个脚注标签就有了指向页面底部对应脚注的链接。那么所剩的就是在脚注中创建一个指向其上下文的链接了。为此,可以使用 .appendTo() 的反向方法.append(),参见代码清单5-16。

代码清单5-16

  1. $(document).ready(function() {
  2. var $notes = $('<ol id="notes"></ol>')
  3. .insertBefore('#footer');
  4. $('span.footnote').each(function(index) {
  5. $(this)
  6. .before([
  7. '<a href="#footnote-',
  8. index + 1,
  9. '" id="context-',
  10. index + 1,
  11. '" class="context">',
  12. '<sup>',
  13. index + 1,
  14. '</sup></a>'
  15. ].join(''))
  16. .appendTo($notes)
  17. .append([
  18. '&nbsp;(<a href="#context-',
  19. index + 1,
  20. '">context</a>)'
  21. ].join(''))
  22. .wrap('<li id="footnote-' + (index + 1) + '"></li>');
  23. });
  24. });

注意,这里的href指向了脚注标签中的id。在图5-9中,可以看到包含新链接的脚注。

5.2 DOM 树操作 - 图7

图 5-9