更聪明的脚本:Scratch提供的优质JavaScript

JavaScript是一种非常有用的语言,具有许多独特的优点。 在不考虑脚本功能如何降低的情况下,您可以使用JavaScript来为网站带来一系列功能,设计和可用性方面的改进。

本文实际上摘自SitePoint的新标题The JavaScript Anthology:101 Essential Tips,Tricks&Hacks 。 这里包括的四章包括:

  • JavaScript的全面介绍,包括基本技术,调试等知识
  • 在JavaScript编程中可以使用文档对象模型的方式
  • 使用框架的实用性,包括使用弹出窗口,在框架之间进行通信以及获取滚动位置的技术
  • 基本DHTML的介绍,其中包括事件处理,光标检测,查找元素的大小和位置等

如果您想离线阅读本入门手册,可以下载PDF格式的章节

但是,现在让我们开始介绍JavaScript,探索它的用途以及如何使用它。

JavaScript定义

JavaScript是一种脚本语言,用于向网页和应用程序添加交互性和动态行为。 JavaScript可以与网页的其他组件(例如HTML和CSS)进行交互,以使它们实时更改或响应用户事件。

毫无疑问,您将在网页的源代码中看到JavaScript。 可能是HTML元素中的内联代码,如下所示:

<a href="page.html" onclick="open('page.html'); return false;">

它可能显示为链接到另一个文件的脚本元素:

<script type="text/javascript" src="myscript.js"></script>

或者它里面可能直接有代码:

<script type="text/javascript"> 
function saySomething(message)
{
 alert(message);
}
saySomething('Hello world!');
</script>

不用担心这些片段之间的差异。 我们可以通过多种方式(好与坏)将JavaScript添加到网页中。 我们将在本章后面详细介绍这些方法。

JavaScript由Netscape开发并在Netscape 2中实现,尽管它最初称为LiveScript。 另一种语言Java的日益流行促使Netscape更改名称,以尝试通过连接获利,因为JavaScript提供了在浏览器和Java小程序之间进行通信的功能。

但是,由于该语言是由Netscape以其原始形式开发的,也是由Microsoft以类似但不同的JScript实现开发的,因此,很明显,Web脚本太重要了,以至于不让厂商竞争。 因此,在1996年,开发工作移交给了名为ECMA的国际标准组织,JavaScript成为ECMAScript或ECMA-262。

大多数人仍然将其称为JavaScript,这可能会引起混乱:除了语法的名称和相似性之外,Java和JavaScript都不一样。

JavaScript的局限性

JavaScript最常用作客户端语言,在这种情况下,“客户端”是指最终用户的Web浏览器,在其中解释并运行JavaScript。 这与服务器端语言(如PHP和ASP)不同,后者在服务器上运行并将静态数据发送到客户端。

由于JavaScript无法访问服务器环境,因此有许多任务虽然在PHP中执行时很琐碎,但是用JavaScript根本无法实现:例如,读取和写入数据库或创建文本文件。 但是,由于JavaScript确实具有访问客户端环境的权限,因此它可以根据服务器端语言根本不具备的数据(例如鼠标的位置或元素的呈现大小)进行决策。

那么ActiveX呢?

如果您已经非常熟悉Microsoft的JScript,您可能会想到“但是JavaScript可以使用ActiveX来完成其中的一些事情”,这是正确的-但ActiveX不属于ECMAScript。 ActiveX是Windows特定的机制,用于允许Internet Explorer访问COM(Windows脚本技术的核心组件对象模型),并且通常仅在受信任的环境(例如Intranet)中运行。 我们将遇到一些特定的例外情况-在IE中运行时没有特殊安全性的ActiveX控件示例(例如Flash插件和XMLHttpRequest)-但在大多数情况下,使用ActiveX编写脚本不在本书的讨论范围之内。

通常,运行客户端的计算机不会像服务器那样强大,因此JavaScript并不是进行大量数据处理的最佳工具。 但是客户端上数据处理的即时性使该选项对少量处理具有吸引力,因为可以立即收到响应。 例如,表单验证非常适合客户端处理。

但是将服务器端和客户端语言与“更好”的视图进行比较是错误的。 两者都不是更好-它们是用于不同工作的工具,它们之间的功能交叉很小。 但是,客户端脚本和服务器端脚本之间不断增强的交互作用催生了新一代的Web脚本,它使用XMLHttpRequest之类的技术来请求服务器数据,运行服务器端脚本,然后在服务器上管理结果。客户端。 我们将在第18章“使用JavaScript构建Web应用程序”中深入研究这些技术。

安全限制

由于JavaScript在高度敏感的数据和程序范围内运行,因此其功能受到限制,以确保不会被恶意使用。 因此,有很多事情是JavaScript根本不允许做的。 例如,它无法从您的计算机读取大多数系统设置,无法直接与硬件进行交互或无法运行程序。

而且,由于该元素的属性,JavaScript中通常不允许特定元素进行某些特定的交互。 例如,更改表单<input>的值
通常是没有问题的,但是如果它是文件输入字段(例如<input type="file"> ),则根本不允许对其进行写操作-一种阻止恶意脚本使用户上传他们没有上传的文件的限制选择。

有很多类似的安全性限制示例,在本书介绍的应用程序中会不断出现,我们将对其进行扩展。 但总而言之,这是JavaScript的主要限制和安全限制的列表,包括我们已经看到的那些限制。 JavaScript无法:

  • 直接打开和读取文件(在特定情况下除外,具体情况除外,如第18章,使用JavaScript构建Web应用程序中所述)。
  • 在用户计算机上创建或编辑文件(Cookie除外,Cookie除外,在第8章中,使用Cookie进行了讨论)。
  • 读取HTTP POST数据。
  • 读取系统设置,或从用户计算机读取的其他任何无法通过语言或宿主对象获得的数据(宿主对象是诸如windowscreen类的东西,它们是环境提供的,而不是语言本身提供的。)
  • 修改文件输入字段的值。
  • 更改从其他域加载的文档的显示。
  • 关闭或修改脚本未打开的窗口(即浏览器主窗口)的工具栏和其他元素。

最终,可能根本不支持JavaScript。

还需要记住的是,许多浏览器包含的选项比仅启用或禁用JavaScript允许的精度更高。 例如,Opera包含一些选项,用于禁止脚本关闭窗口,移动窗口,写入状态栏,接收右键单击……列表继续进行。 您几乎无能为力,但是,大多数情况下,您不需要这样做。此类选项已经演变为可以抑制“烦人”的脚本(状态栏滚动条,无右键单击的脚本等),因此,如果您不这样做,从这类脚本中,问题很少出现。

JavaScript最佳做法

JavaScript最佳做法特别强调了以下问题:对于浏览器不支持脚本,脚本已关闭或由于其他原因而无法与脚本进行交互的用户(应该由用户使用)不支持脚本的辅助技术)。

最后一个问题最难解决,我们将在第16章“ JavaScript和可访问性”中集中讨论该问题的解决方案。 在这一部分中,我想看看好的JavaScript的三个核心原则:

  • 渐进增强–为没有JavaScript的用户提供
  • 简洁的脚本-将内容与行为分开
  • 一致的编码实践–使用花括号和分号终止符

第一个原则是确保每当我们在网站上使用脚本时,我们都在考虑更大的前景。 第二点使我们的维护变得更容易,并为用户带来更好的可用性和顺畅的降级 。 (优雅的降级意味着,如果不支持JavaScript,浏览器自然会退回到非脚本功能或“降级”到非脚本功能。)第三个原理使代码更易于阅读和维护。

提供给没有JavaScript的用户(渐进增强)

用户可能不使用JavaScript的原因有很多:

  • 他们使用的设备根本不支持脚本,或者仅以有限的方式支持脚本。
  • 它们位于可过滤JavaScript的代理服务器或防火墙的后面。
  • 他们故意关闭了JavaScript。

第一点涵盖了令人惊讶的,规模不断扩大的设备范围,包括PDA等小屏幕设备,WebTV和Sony PSP等中屏幕设备以及Opera 5和Netscape 4等旧式JavaScript浏览器。

上面列表中的最后一点可以说是可能性最小(除了其他开发者扮演恶魔的拥护者!),但是原因并不那么重要:某些用户只是没有JavaScript,我们应该容纳它们。 无法量化属于该类别的用户数量,因为众所周知,从服务器检测JavaScript支持并不可靠,但是我看到的数字显示,关闭JavaScript的用户比例在5%到20%之间,取决于您是否将搜索引擎机器人描述为“用户”。

解决此问题的长期方法是使用HTML noscript元素,其内容由根本不支持script元素的浏览器以及支持该元素但已关闭脚本的浏览器呈现。

尽管这是一个好主意,但实际上,随着时间的流逝,此解决方案的用处逐渐减少,因为noscript无法按功能区分。 提供有限JavaScript支持的浏览器将无法运行复杂的脚本,但是此类设备是具有脚本功能的浏览器,因此它们也不会解析noscript元素。 这些浏览器最终将一无所有。

解决此问题的更好方法是从静态HTML开始,然后使用脚本在该静态内容内修改或添加动态行为。

让我们看一个简单的例子。 制作DHTML菜单的首选技术使用无序列表作为主菜单结构。 我们将在第15章中介绍DHTML菜单和导航的全部内容,但是这个简短的示例说明了这一点:

<ul id="menu"> 
 <li><a href="/">Home</a></li>
 <li><a href="/about/">About</a></li>
 <li><a href="/contact/">Contact</a></li>
</ul>

<script type="text/javascript" src="menu.js"></script>

链接列表是纯HTML格式,因此所有用户都可以使用,无论他们是否启用了脚本。 如果支持脚本,我们的menu.js脚本可以应用动态行为,但是如果不支持脚本,则内容仍会出现。 我们没有明确区分设备-我们只是提供了内容,如果浏览器可以处理,则该内容是动态的;如果不能,则提供静态的内容。

讨论区

这种情况的“传统”方法是在纯JavaScript中生成单独的动态菜单,并在noscript元素内包含备用静态内容:

<script type="text/javascript" src="menu.js"></script> 

<noscript>
 <ul>
   <li><a href="/">Home</a></li>
   <li><a href="/about/">About</a></li>
   <li><a href="/contact/">Contact</a></li>
 </ul>
</noscript>

但是,正如我们已经看到的那样,由于JavaScript支持不再是“全有或全无”的主张,因此各种各样的设备都将落入这个网络。 上面的方法为所有设备提供默认内容,并且仅在可行时才应用脚本功能。

这种脚本编写方法通常被称为渐进增强 ,它是我们整本书将使用的一种方法。

不要问!

不得使用此技术或noscript元素来添加一条消息,即“请打开JavaScript以继续”。 这种消息充其量是至高无上的(“我为什么要?”); 在最坏的情况下,它可能毫无帮助(“我不能!”)或毫无意义(“什么是JavaScript?”)。 就像那些标有“请升级您的浏览器”的启动页面一样,这些消息对普通Web用户来说也和标有“请使用其他汽车”的路标一样有用。

有时,您可能会面临这样的情况,即如果没有JavaScript,就无法提供等效的功能。 在这种情况下,我认为可以有一条静态消息通知用户这种不兼容性(当然是非技术性的说法)。 但是,在大多数情况下,除非确实是唯一的方法,否则请尽量避免提供此类消息。

从行为中分离内容(非干扰性脚本)

将内容与行为分开意味着将网页构造的不同方面分开。 杰弗里·扎德曼(Jeffrey Zeldman)称其为Web开发的“三足凳”(Zeldman,J. Web标准设计,New Riders,2003年),包括内容(HTML),演示文稿(CSS)和行为(JavaScript)-这不仅强调了每个方面的功能差异,还强调了它们应该彼此分开的事实。

良好的分隔性使网站易于维护,更易于访问,并且在较旧或规格较低的浏览器中也能很好地降级。

在一个与将内容与行为分开的理想直接对立的极端情况下,我们可以在属性事件处理程序中直接编写内联代码。 这很混乱,通常应避免:

<div id="content" 
   onmouseover="this.style.borderColor='red'"
   onmouseout="this.style.borderColor='black'">

我们可以通过采用完成工作的代码并将其抽象为一个函数来改善这种情况:

<div id="content" 
   onmouseover="changeBorder('red')"
   onmouseout="changeBorder('black')">

定义一个为我们完成工作的函数使我们可以在单独的JavaScript文件中提供大部分代码:

Example 1.1. separate-content-behaviors.js (excerpt) 

function changeBorder(element, to)
{
 element.style.borderColor = to;
}

但是更好的方法是避免完全使用内联事件处理程序。 相反,我们可以利用文档对象模型(DOM)将事件处理程序绑定到HTML文档中的元素。 DOM是一个标准的编程接口,像JavaScript这样的语言可以通过它访问HTML文档的内容,而无需在HTML文档本身中显示任何JavaScript代码。 在此示例中,我们的HTML代码如下所示:

<div id="content">

这是我们将使用的脚本:

Example 1.2. separate-content-behaviors.js 

function changeBorder(element, to)
{
 element.style.borderColor = to;
}

var contentDiv = document.getElementById('content');

contentDiv.onmouseover = function()
{
 changeBorder('red');
};

contentDiv.onmouseout = function()
{
 changeBorder('black');
};

这种方法使我们无需编辑HTML即可添加,删除或更改事件处理程序,并且由于文档本身完全不依赖或不引用脚本,因此不了解JavaScript的浏览器将不受此影响。 。 该解决方案还提供了可重用性的好处,因为我们可以根据需要将相同的功能绑定到其他元素,而无需编辑HTML。

该解决方案取决于我们通过DOM访问元素的能力,这将在第5章“浏览文档对象模型”中进行深入介绍。

分离的好处

通过对内容和行为进行良好的分离,我们不仅获得了从更顺畅的降级方面的实际收益,而且还获得了在分离方面进行思考的优势。 由于我们将HTML和JavaScript分开了,而不是将它们组合在一起,因此在查看HTML时,我们不太可能会忘记其核心功能应该是描述页面的内容,而与任何脚本无关。

安迪·克拉克(Andy Clarke)提到了Web标准琐事 ,这是一个有用的类比,琐事看起来是一个好的网站应有的方式:当您看着碗时,您会看到构成甜点的所有单独的层。 相反的可能是水果蛋糕:当您查看蛋糕时,您无法分辨每种成分是什么。 您只能看到一大堆蛋糕。

讨论区

重要的是要注意,当您将事件处理程序绑定到这样的元素上时,您必须等到该元素实际存在后才能执行此操作。 如果将前面的脚本原样放在页面的开头部分,它将报告错误并且无法正常工作,因为在处理脚本的那一刻尚未呈现内容div。

最直接的解决方案是将代码放入加载事件处理程序中。 在那里永远是安全的,因为直到完全渲染文档之后才触发load事件:

window.onload = function() 
{
 var contentDiv = document.getElementById('content');

 ...
};

或者更清楚一点,输入更多:

window.onload = init; 

function init()
{
 var contentDiv = document.getElementById('content');

 ...
}

加载事件处理程序的问题在于,页面上只有一个脚本可以使用它。 如果两个或多个脚本尝试安装装入事件处理程序,则每个脚本将覆盖它之前的脚本的处理程序。 解决此问题的方法是以更现代的方式响应负载事件。 我们将在“获取多个脚本以在同一页面上工作”部分中对此进行简要介绍。

使用花括号和分号(一致的编码实践)

在许多JavaScript操作中,花括号和分号是可选的,因此当它们不是必需的时将它们包括在内是否有任何价值?

尽管括号和分号通常是可选的,但您应始终包括它们。 这使代码更容易被他人和将来的人阅读,并在重用和重新组织脚本中的代码时避免出现问题(这通常会使可选的分号成为必需)。

例如,此代码完全有效:

Example 1.3. semicolons-braces.js (excerpt) 

if (something) alert('something')
else alert('nothing')

由于JavaScript解释器中称为分号插入的过程,因此该代码有效。 每当解释器找到两个由一个或多个换行符分隔的代码段,并且如果这些代码段在同一行上就没有意义,那么解释程序就将它们视为在它们之间存在分号。 通过类似的机制,可以从语法中推断出通常在if-else语句中要执行的代码周围的花括号,即使它们不存在。 将此过程视为解释器为您添加缺少的代码元素。

即使不一定总是需要这些代码元素,但如果您始终使用它们,则更容易记住在需要时使用它们,并且更容易阅读生成的代码。

上面的示例最好像这样编写:

Example 1.4. semicolons-braces.js (excerpt) 

if (something) { alert('something'); }
else { alert('nothing'); }

此版本代表最终的代码可读性:

Example 1.5. semicolons-braces.js (excerpt) 

if (something)
{
 alert('something');
}
else
{
 alert('nothing');
}

使用函数字面量

随着您对JavaScript语言的复杂性的了解,使用函数文字根据需要创建匿名函数并将它们分配给JavaScript变量和对象属性将变得很普遍。 在这种情况下,函数定义后应加上分号,以分号终止变量分配:

var saySomething = function(message)
{
...
};

向页面添加脚本

在脚本开始执行令人兴奋的事情之前,您必须将其加载到网页中。 有两种技术可以做到这一点,其中一种明显优于另一种。

如前所述,第一种也是最直接的技术是直接在script元素内编写代码:

<script type="text/javascript"> 
function saySomething(message)
{
 alert(message);
}

saySomething('Hello world!');
</script>

这种方法的问题在于,在传统浏览器和纯文本浏览器(完全不支持script元素的浏览器)中,内容可能呈现为原义文本。

避免此问题的更好的选择是始终将脚本放入外部JavaScript文件中。 看起来像这样:

<script type="text/javascript" src="what-is-javascript.js" 
   ></script>

这将加载一个名为what-is-javascript.js的外部JavaScript文件。 该文件应包含否则应放在script元素中的代码,如下所示:

Example 1.6. what-is-javascript.js 

function saySomething(message)
{
 alert(message);
}

saySomething('Hello world!');

当您使用此方法时,不了解script元素的浏览器将忽略它,并且不呈现任何内容(因为该元素为空),但是懂得script元素的浏览器将加载和处理脚本。 这有助于使脚本和内容保持分离,并且易于维护-您可以在多个页面上使用相同的脚本,而不必在多个文档中维护代码的副本。

讨论区

您可能会对不直接在script元素内使用代码的建议提出质疑。 您可能会说:“没问题。” “我将围绕它添加HTML注释。” 好吧,我不得不不同意这一点:使用HTML注释“隐藏”代码是一个非常坏的习惯,我们应该避免陷入这种习惯。

将HTML注释放在代码周围

验证解析器不需要阅读注释,更不用说处理注释了。 带注释的JavaScript完全起作用的事实是过时的,这是对古老的过时实践的回溯,这种过时假设对文档可能是不正确的:它假定将页面提供给非验证解析器。

本书中的所有示例均以HTML格式(而不是XHTML)提供,因此这种假设是合理的,但是如果您使用的是XHTML(正确使用MIME类型的application / xhtml + xml),则您的注释在浏览器处理文档之前,验证XML解析器可能会丢弃这些代码,在这种情况下,已注释的脚本将不再可用。 为了确保向前兼容(以及对您自己的编码习惯以及对单个项目的相关好处),我强烈建议您避免以这种方式在代码周围添加注释。 您的JavaScript应该始终存储在外部JavaScript文件中。

language属性

语言属性不再是必需的。 在Netscape 4及其同时代的浏览器占主导地位的时代, <script>标记的language属性起着嗅探上级支持的作用(例如,通过指定javascript1.3 ),并影响了这种方式的一些小方面。脚本解释器起作用了。

但是,既然JavaScript是ECMAScript,并且现在不推荐使用language属性,而是使用type属性,那么指定JavaScript版本就变得毫无意义。 此属性指定所包含文件的MIME类型,例如脚本和样式表,并且是唯一需要使用的属性:

<script type="text/javascript">

从技术上讲,该值应为text/ecmascript ,但Internet Explorer无法理解。 就个人而言,如果这样做的话,我会更高兴,仅仅是因为javascript是(讽刺地)我很难输入的一个单词-我已经记不清脚本失败发生的次数了,因为我键入了type="text/javsacript"

使多个脚本在同一页面上工作

当多个脚本不能一起工作时,几乎总是因为脚本想要为给定元素上的同一事件分配事件处理程序。 由于每个元素每个事件只能有一个处理程序,因此脚本会覆盖彼此的事件处理程序。

常见的怀疑对象是窗口对象的加载事件处理程序,因为页面上只有一个脚本可以使用此事件。 如果两个或多个脚本正在使用它,则最后一个脚本将覆盖之前的脚本。

我们可以从单个加载处理程序内部调用多个函数,如下所示:

window.onload = function()  
{  
 firstFunction();  
 secondFunction();  
}

但是,如果我们使用此代码,则将被束缚在一段代码上,而在加载时,我们将必须执行该代码。 更好的解决方案将提供一种添加与其他处理程序不冲突的加载事件处理程序的方法。

当调用以下单个函数时,它将允许我们分配任意数量的加载事件处理程序,而不会发生冲突:

Example 1.7. add-load-listener.js  
 
function addLoadListener(fn)  
{  
 if (typeof window.addEventListener != 'undefined')  
 {  
   window.addEventListener('load', fn, false);  
 }  
 else if (typeof document.addEventListener != 'undefined')  
 {  
   document.addEventListener('load', fn, false);  
 else if (typeof window.attachEvent != 'undefined')  
 {  
   window.attachEvent('onload', fn);  
 }  
 else  
 {  
   var oldfn = window.onload;  
   if (typeof window.onload != 'function')  
   {  
     window.onload = fn;  
   }  
   else  
   {  
     window.onload = function()  
     {  
       oldfn();  
       fn();  
     };  
   }  
 }  
}

启用此功能后,我们可以多次使用它:

addLoadListener(firstFunction);  
addLoadListener(secondFunction);  
addLoadListener(twentyThirdFunction);

你明白了!

讨论区

JavaScript包括用于添加(和删除)事件侦听器的方法,这些方法的运行方式与事件处理程序非常相似,但是允许多个侦听器订阅元素上的单个事件。 不幸的是,Internet Explorer中事件侦听器的语法与其他浏览器完全不同:在IE使用专有方法的情况下,IE使用W3C标准。 我们将经常遇到这种二分法,我们将在第13章“基本动态HTML”中进行详细讨论。

W3C标准方法称为addEventListener

window.addEventListener('load', firstFunction, false);

IE方法称为attachEvent

window.attachEvent('onload', firstFunction);

如您所见,标准构造使用事件的名称(不带“ on”前缀),后接事件发生时要调用的函数,以及控制事件冒泡的参数(请参见第13章,基本动态)。 HTML,以获取更多详细信息)。 IE方法采用事件处理程序名称(包括“ on ”前缀),后跟函数名称。

为了将它们组合在一起,我们需要添加一些测试,以在尝试使用每种方法之前检查每种方法是否存在。 我们可以使用JavaScript运算符typeof来执行此操作,该操作符可识别不同类型的数据(如"string""number""boolean""object""array""function""undefined" )。 不存在的方法将返回"undefined"

if (typeof window.addEventListener != 'undefined')  
{  
 ... window.addEventListener is supported  
}

还有一个复杂的问题:在Opera中,可以触发多个事件侦听器的load事件来自文档对象,而不是窗口。 但是我们不能只使用文档,因为这在较旧的Mozilla浏览器(例如Netscape 6)中不起作用。 要绘制通过这些怪癖的路线,我们需要依次测试window.addEventListenerdocument.addEventListenerwindow.attachEvent

最后,对于不支持上述任何方法的浏览器(实际上是Mac IE 5),后备解决方案是将多个旧式事件处理程序链接在一起,以便在事件发生时依次调用它们。 我们通过动态构造一个新的事件处理程序来做到这一点,该事件处理程序会在事件发生时在调用新分配的处理程序之前调用任何现有的处理程序。 (此技术由Simon Willison率先提出 。)

Example 1.8. add-load-listener.js (excerpt)  
 
var oldfn = window.onload;  
if (typeof window.onload != 'function')  
{  
 window.onload = fn;  
}  
else  
{  
 window.onload = function()  
 {  
   oldfn();  
   fn();  
 };  
}

如果您不了解其工作原理,请不要担心-我们将在第13章“基本动态HTML”中更详细地探讨所涉及的技术。 在那里,我们将学习事件侦听器不仅对load事件有用,而且对任何类型的事件驱动脚本都有用。

隐藏JavaScript源代码

如果您曾经创造过引以为傲的产品,那么您就会理解保护知识产权的渴望。 但是,Web上的JavaScript本质上是一种开源语言。 它以其源代码形式出现在浏览器中,因此,如果浏览器可以运行它,则一个人可以阅读它。

Web上有一些声称提供源代码加密的应用程序,但是实际上,您无法加密其他编码器无法在几秒钟内解密的源代码。 实际上,其中一些程序实际上会引起问题:它们经常以使速度更慢,效率更低或只是断断续续的方式重新格式化代码。 我的建议? 像瘟疫一样远离他们。

但是,仍然需要隐藏代码。 您可以采取一些措施来混淆用户可以看到的代码(如果不是完全加密的话)。

除去所有注释和不必要的空格的代码非常难于阅读,并且正如您所期望的那样,从此类代码中提取单个功能非常困难。 以这种方式压缩脚本的简单技术可以推迟除最坚定的黑客之外的所有黑客。 例如,使用以下代码:

Example 1.9. obfuscate-code.js (excerpt)  
 
var oldfn = window.onload;  
if (typeof window.onload != 'function')  
{  
 window.onload = fn;  
}  
else  
{  
 window.onload = function()  
 {  
   oldfn();  
   fn();  
 };  
}

我们只需删除不必要的空格就可以将该代码压缩为以下两行:

Example 1.10. obfuscate-code.js (excerpt)  
 
var oldfn=window.onload;if(typeof window.onload!='function'){  
window.onload=fn;}else{window.onload=function(){oldfn();fn();};}

但是,请记住该重要词-不必要。 一些空白是必不可少的,例如vartypeof之后的单个空格。

讨论区

除了混淆的好处外,这种做法还有很多优点。 除去注释和不必要的空格的脚本较小; 因此,它们的加载速度更快,并且处理速度可能更快。

但是请记住,必须使用分号行终止符和大括号对代码进行严格格式化(正如我们在“使用大括号和分号(一致的编码实践)”一节中所讨论的); 否则,删除换行符将使代码行一起运行,并最终导致错误。

开始压缩之前,请记住要复制脚本。 我知道这似乎很明显,但是我已经多次犯了这个错误,而成为这样的初学者更是令人振奋! 这些天,我要做的是以完整的空格和注释形式编写和维护脚本,然后在发布之前通过一堆搜索/替换表达式运行它们。 通常,我保留脚本的两个副本,分别名为myscript.jsmyscript-commented.js或类似名称。

我们将在第20章,保持节奏中返回本主题,在其中我们将讨论一系列技术,这些技术可提高脚本的速度和效率,并减少脚本所需的物理空间。

调试脚本

调试是发现并(希望)修复错误的过程。 大多数浏览器都内置了某种错误报告,还有一些外部调试器也值得研究。

了解浏览器的内置错误报告

Opera,Mozilla浏览器(例如Firefox)和Internet Explorer都内置了不错的错误报告功能,但是Opera和Mozilla的调试工具最有用。

歌剧
从工具>高级> JavaScript控制台打开JavaScript控制台。 您还可以将其设置为在发生错误时自动打开,方法是转到工具>首选项>高级>内容,然后单击JavaScript选项按钮以打开其对话框,然后选中在错误时打开JavaScript控制台。

Firefox和其他Mozilla浏览器
从工具> JavaScript控制台中打开JavaScript控制台。

Windows版Internet Explorer
转到工具> Internet选项>高级,然后取消选中禁用脚本调试选项,然后选中显示有关每个脚本错误的通知选项,以在出现错误时弹出对话框。

Mac版Internet Explorer
转到资源管理器>首选项> Web浏览器> Web内容,然后选中显示脚本错误警报选项。

Safari默认不包含错误报告,但是最新版本具有“秘密”调试菜单,其中包括JavaScript控制台,您可以通过输入以下Terminal命令来启用该菜单。 ( $表示命令提示符,请勿键入。)

$ defaults write com.apple.safari IncludeDebugMenu -bool true

您还可以使用一个名为Safari Enhancer的扩展程序,其中包括一个将JavaScript消息转储到Mac OS控制台的选项。 但是,这些消息不是很有帮助。

了解各种浏览器的控制台消息可能需要一些实践,因为每种浏览器都会提供不同的信息。 这是一个错误的示例-错误的函数调用:

function saySomething(message)  
{  
 ...  
 alert(message);  
}  
saySometing('Hello world');

Firefox提供了一个简洁但非常准确的报告,其中包括发生错误的行号以及说明,如图1.1“ Firefox中的JavaScript错误控制台”所示。

Firefox的JavaScript控制台
图1.1。 Firefox中的JavaScript错误控制台

如图1.2“ Opera中的JavaScript控制台”所示,Opera提供了一个非常冗长的报告,其中包括对该错误产生源的事件的回溯,该错误发生的行的通知以及说明。

当最初由其他代码调用的代码中发生错误时,回溯将有所帮助。 例如,事件处理程序调用一个函数,然后又调用第二个函数,此时就发生了错误。 Opera's console will trace this process back through each stage to its originating event or call.

Internet Explorer gives the fairly basic kind of report shown in Figure 1.3, "The JavaScript console in Windows IE". It provides the number of the line at which the interpreter encountered the error (this may or may not be close to the true location of the actual problem), plus a summary of the error type, though it doesn't explain the specifics of the error itself. (Internet Explorer is particularly bad at locating errors in external JavaScript files. Often, the line number it will report as the error location will actually be the number of the line at which the script is loaded in the HTML file.)

Opera's JavaScript Console
Figure 1.2. The JavaScript console in Opera

IE's JavaScript Console
Figure 1.3. The JavaScript console in Windows IE

As you probably gathered, I'm not overly impressed by Internet Explorer's error reporting, but it is vastly better than nothing: at least you know that an error has occurred.

Using alert

The alert function is a very useful means of analyzing errors — you can use it at any point in a script to probe objects and variables to see if they contain the data you expect. For example, if you have a function that has several conditional branches, you can add an alert within each condition to find out which is being executed:

Example 1.11. debugging-dialogs.js  
 
function checkAge(years)  
{  
 if (years < 13)  
 {  
   alert('less than 13');  
 
   ... other scripting  
 }  
 else if (years >= 13 && years <= 21)  
 {  
   alert('13 to 21');  
 
   ... other scripting  
 }  
 else  
 {  
   alert('older');  
 
   ... other scripting  
 }  
}

Maybe the value for years is not coming back as a number, like it should. You could add to the start of your script an alert that tests the variable to see what type it is:

function checkAge(years)  
{  
 alert(typeof years);  
 ...

In theory, you can put any amount of information in an alert dialog, although a very long string of data could create such a wide dialog that some of the information would be clipped or outside the window. You can avoid this by formatting the output with escape characters, such as n for a line break.

Using try-catch

The try-catch construct is an incredibly useful way to get a script just to "try something," leaving you to handle any errors that may result. The basic construct looks like this:

Example 1.12. debugging-trycatch.js (excerpt)  
 
try  
{  
 ... some code  
}  
catch (err)  
{  
 ... this gets run if the try{} block results in an error  
}

If you're not sure where an error's coming from, you can wrap a try-catch around a very large block of code to trap the general failure, then tighten it around progressively smaller chunks of code within that block. For example, you could wrap a try brace around the first half of a function (at a convenient point in the code), then around the second half, to see where the error occurs; you could then divide the suspect half again, at a convenient point, and keep going until you've isolated the problematic line.

 catch has a single argument (I've called it err in this case), which receives the error object; we can query properties of that object, such as name and message, to get details about the error.

Often, I use a for-in iterator to run through the entire object and find out what it says:

Example 1.13. debugging-trycatch.js (excerpt)  
 
for (var i in err)  
{  
 alert(i + ': ' + err[i]);  
}

Writing to the Page or Window

If you're examining a great deal of data while debugging, or you're dealing with data that's formatted in a complicated way, it's often better to write that data directly to a page or popup window than to try to deal with lots of alert dialogs. If you're examining data in a loop, in particular, you could end up generating hundreds of dialogs, each of which you'll have to dismiss manually?a very tedious process.

In these kinds of situations, we can use an element's innerHTML property to write the data to the page. Here's an example in which we build a list using the contents of an array (data), then write it into a test div:

Example 1.14. debugging-writing.js (excerpt)  
 
var test = document.getElementById('testdiv');  
 
test.innerHTML += '<ul>';  
for (var i = 0; i < data.length; i++)  
{  
 test.innerHTML += '<li>' + i + '=' + data[i] + '</li>';  
}  
test.innerHTML += '</ul>';

We can also write the data into a popup, which is useful if there's no convenient place to put it on the page:

Example 1.15. debugging-writing.js (excerpt)  
 
var win = window.open('', win, 'width=320,height=240');  
 
win.document.open();  
win.document.write('<ul>');  
for (var i = 0; i < data.length; i++)  
{  
 win.document.write('<li>' + i + '=' + data[i] + '</li>')  
}  
win.document.write('</ul>');  
win.document.close();

You can format the output however you like, and use it to structure data in any way that makes it easier for you to find the error.

When you're working with smaller amounts of data, you can gain a similar advantage by writing the data to the main title element:

Example 1.16. debugging-writing.js (excerpt)  
 
document.title = '0 = ' + data[0];

This final approach is most useful when tracking data that changes continually or rapidly, such as a value being processed by a setInterval function (an asynchronous timer we'll meet properly in Chapter 14, Time and Motion).

Using an External Debugger

I can recommend two debuggers:

  • Venkman for Mozilla and Firefox
  • Microsoft Script Debugger for Windows Internet Explorer

External debuggers are a far more detailed way to analyze your scripts, and have much greater capabilities than their in-browser counterparts. External debuggers can do things like stopping the execution of the script at specific points, or watching particular properties so that you're informed of any change to them, however it may be caused. They also include features that allow you "step through" code line by line, in order help find errors that may occur only briefly, or are otherwise difficult to isolate.

External debuggers are complex pieces of software, and it can take time for developers to learn how to use them properly. They can be very useful for highlighting logical errors, and valuable as learning tools in their own right, but they're limited in their ability to help with browser incompatibilities: they're only useful there if the bug you're looking for is in the browser that the debugger supports!

Strict Warnings

If you open the JavaScript console in Firefox you'll see that it includes options to show Errors and Warnings. Warnings notify you of code that, though it is not erroneous per se, does rely on automatic error handling, uses deprecated syntax, or is in some other way untrue to the ECMAScript specification. (To see these warnings, it may be necessary to enable strict reporting by typing in the address about:config and setting javascript.options.strict to true .)

For example, the variable fruit is defined twice in the code below:

Example 1.17. strict-warnings.js (excerpt)  
 
var fruit = 'mango';  
 
if (basket.indexOf('apple') != -1)  
{  
 var fruit = 'apple';  
}

We should have omitted the second var , because var is used to declare a variable for the first time, which we've already done. Figure 1.4, "The JavaScript warnings console in Firefox" shows how the JavaScript console will highlight our error as a warning.

Firefox's JavaScript Warnings Console
Figure 1.4. The JavaScript warnings console in Firefox

There are several coding missteps that can cause warnings like this. 例如:

  • re-declaring a variable - This produces the warning, "redeclaration of var name," as we just saw.
  • failing to declare a variable in the first place - This oversight produces the warning, "assignment to undeclared variable name." This might arise, for example, if the first line of our code read simply fruit = 'mango';
  • assuming the existence of an object - This assumption produces the warning "reference to undefined property name."

For example, a test condition like if (document.getElementById) assumes the existence of the getElementById method, and banks on the fact that JavaScript's automatic error-handling capabilities will convert a nonexistent method to false in browsers in which this method doesn't exist. To achieve the same end without seeing a warning, we would be more specific, using if(typeof document.getElementById != 'undefined') .

There are also some function-related warnings, and a range of other miscellaneous warnings that includes my personal favorite, "useless expression," which is produced by a statement within a function that does nothing:

Example 1.18. strict-warnings.js (excerpt)  
 
function getBasket()  
{  
 var fruit = 'pomegranate';  
 fruit;  
}

For a thorough rundown on the topic, I recommend Alex Vincent's article Tackling JavaScript strict warnings .

Warnings don't matter in the sense that they don't prevent our scripts from working, but working to avoid warnings helps us to adopt better coding practice, which ultimately creates efficiency benefits. For instance, scripts run faster in Mozilla if there are no strict warnings, a subject we'll look at again in Chapter 20, Keeping up the Pace.

Type Conversion Testing

Although we shouldn't rely on type conversion to test a value that might be undefined, it's perfectly fine to do so for a value that might be null, because the ECMAScript specification requires that null evaluates to false. So, for example, having already established the existence of getElementById using the typeof operator as shown above, it's perfectly safe from then on to test for individual elements as shown below, because getElementById returns null for nonexistent elements in the DOM:

if (document.getElementById('something'))  
{  
 ... the element exists  
}
摘要

In this chapter, we've talked about best-practice approaches to scripting that will make our code easier to read and manage, and will allow it to degrade gracefully in unsupported devices. We've also begun to introduce some of the techniques we'll need to build useful scripts, including the ubiquitous load event listener that we'll use for almost every solution in this book!

We've covered some pretty advanced stuff already, so don't worry if some of it was difficult to take in. We'll be coming back to all the concepts and techniques we've introduced here as we progress through the remaining chapters.

Chapter 5. Navigating the Document Object Model

Browsers give JavaScript programs access to the elements on a web page via the Document Object Model (DOM) -- an internal representation of the headings, paragraphs, lists, styles, IDs, classes, and all the other data to be found in the HTML on your page.

The DOM can be thought of as a tree consisting of interconnected nodes. Each tag in an HTML document is represented by a node; any tags that are nested inside that tag are nodes that are connected to it as children, or branches in the tree. Each of these nodes is called an element node. (Strictly speaking, each element node represents a pair of tags - the start and end tags of an element (eg, <p> and </p> ) - or a single self-closing tag (eg, <br> , or <br/> in XHTML).) There are several other types of nodes; the most useful are the document node, text node, and attribute node. The document node represents the document itself, and is the root of the DOM tree. Text nodes represent the text contained between an element's tags. Attribute nodes represent the attributes specified inside an element's opening tag. Consider this basic HTML page structure:

<html>   
 <head>  
   <title>Stairway to the stars</title>  
 </head>  
 <body>  
   <h1 id="top">Stairway to the stars</h1>  
   <p class="introduction">For centuries, the stars have been  
     more to humankind than just burning balls of gas ...</p>  
 </body>  
</html>

The DOM for this page could be visualized as Figure 5.1, "The DOM structure of a simple HTML page, visualized as a tree hierarchy".

Every page has a document node, but its descendents are derived from the content of the document itself. Through the use of element nodes, text nodes, and attribute nodes, every piece of information on a page is accessible via JavaScript.

The DOM isn't just restricted to HTML and JavaScript, though. Here's how the W3C DOM specification site explains the matter:

The Document Object Model is a platform- and language-neutral interface that will allow programs and scripts to dynamically access and update the content, structure and style of documents.

So, even though the mixture of JavaScript and HTML is the most common combination of technologies in which the DOM is utilized, the knowledge you gain from this chapter can be applied to a number of different programming languages and document types.

In order to make you a "master of your DOMain," this chapter will explain how to find any element you're looking for on a web page, then change it, rearrange it, or erase it completely.

The DOM structure of a simple HTML page
Figure 5.1. The DOM structure of a simple HTML page, visualized as a tree hierarchy

Accessing Elements

Access provides control, control is power, and you're a power programmer, right? So you need access to everything that's on a web page. Fortunately, JavaScript gives you access to any element on a page using just a few methods and properties.

Solution

Although it's possible to navigate an HTML document like a road map?starting from home and working your way towards your destination one node at a time?this is usually an inefficient way of finding an element because it requires a lot of code, and any changes in the structure of the document will usually mean that you have to rewrite your scripts. If you want to find something quickly and easily, the method that you should tattoo onto the back of your hand is document.getElementById .

Assuming that you have the correct markup in place, getElementById will allow you immediately to access any element by its unique id attribute value. For instance, imagine your web page contains this code:

Example 5.1. access_element.html (excerpt)   
 
<p>  
 <a id="sirius" href="sirius.html">Journey to the stars</a>  
</p>

You can use the a element's id attribute to get direct access to the element itself:

Example 5.2. access_element.js (excerpt)   
 
var elementRef = document.getElementById("sirius");

The value of the variable elementRef will now be referenced to the a element -- any operations that you perform on elementRef will affect that exact hyperlink.

 getElementById is good for working with a specific element; however, sometimes you'll want to work with a group of elements. In order to retrieve a group of elements on the basis of their tag names, you can use the method getElementsByTagName .

As can be seen from its name, getElementsByTagName takes a tag name and returns all elements of that type. Assume that we have this HTML code:

Example 5.3. access_element2.html (excerpt)   
 
<ul>  
 <li>  
   <a href="sirius.html">Sirius</a>  
 </li>  
 <li>  
   <a href="canopus.html">Canopus</a>  
 </li>  
 <li>  
   <a href="arcturus.html">Arcturus</a>  
 </li>  
 <li>  
   <a href="vega.html">Vega</a>  
 </li>  
</ul>

We can retrieve a collection that contains each of the hyperlinks like so:

Example 5.4. access_element2.js (excerpt)   
 
var anchors = document.getElementsByTagName("a");

The value of the variable anchors will now be a collection of a elements. Collections are similar to arrays in that each of the items in a collection is referenced using square bracket notation, and the items are indexed numerically starting at zero. The collection returned by getElementsByTagName sorts the elements by their source order, so we can reference each of the links thus:

anchorArray[0]   
 
   the a element for "Sirius"  
anchorArray[1]  
 
   the a element for "Canopus"  
anchorArray[2]  
 
   the a element for "Arcturus"  
anchorArray[3]  
 
   the a element for "Vega"

Using this collection you can iterate through the elements and perform an operation on them, such as assigning a class using the element nodes' className property:

Example 5.5. access_element2.js (excerpt)   
 
var anchors = document.getElementsByTagName("a");  
 
for (var i = 0; i < anchors.length; i++)  
{  
 anchors[i].className = "starLink";  
}

Unlike getElementById , which may be called on the document node only, the getElementsByTagName method is available from every single element node. You can limit the scope of the getElementsByTagName method by executing it on a particular element. getElementsByTagName will only return elements that are descendents of the element on which the method was called.

If we have two lists, but want to assign a new class to the links in one list only, we can target those a elements exclusively by calling getElementsByTagName on their parent list:

Example 5.6. access_element3.html (excerpt)   
 
<ul id="planets">  
 <li>  
   <a href="mercury.html">Mercury</a>  
 </li>  
 <li>  
   <a href="venus.html">Venus</a>  
 </li>  
 <li>  
   <a href="earth.html">Earth</a>  
 </li>  
 <li>  
   <a href="mars.html">Mars</a>  
 </li>  
</ul>  
<ul id="stars">  
 <li>  
   <a href="sirius.html">Sirius</a>  
 </li>  
 <li>  
   <a href="canopus.html">Canopus</a>  
 </li>  
 <li>  
   <a href="arcturus.html">Arcturus</a>  
 </li>  
 <li>  
   <a href="vega.html">Vega</a>  
 </li>  
</ul>

To target the list of stars, we need to obtain a reference to the parent ul element, then call getElementsByTagName on it directly:

Example 5.7. access_element3.js (excerpt)   
 
var starsList = document.getElementById("stars");  
var starsAnchors = starsList.getElementsByTagName("a");

The value of the variable starsAnchors will be a collection of the a elements inside the stars unordered list, instead of a collection of all a elements on the page.

DOM 0 Collections

Many "special" elements in an HTML document can be accessed by even more direct means. The body element of the document can be accessed as document.body. A collection of all the forms in a document may be found in document.forms . All of the images in a document may be found in document.images .

In fact, most of these collections have been around since before the DOM was standardized by the W3C, and are commonly referred to as DOM 0 properties.

Because the initial implementations of these features were not standardized, these collections have occasionally proven unreliable in browsers that are moving towards standards compliance. Early versions of some Mozilla browsers (e.g., Firefox), for example, did not support these collections on XHTML documents.

Today's browsers generally do a good job of supporting these collections; however, if you do run into problems, it's worth trying the more verbose getElementsByTagName method of accessing the relevant elements. Instead of document.body , for example, you could use:

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

Discussion

If you really need to step through the DOM hierarchy element by element, each node has several properties that enable you to access related nodes:

  • node.childNodes - a collection that contains source-order references to each of the children of the specified node, including both elements and text nodes
  • node.firstChild - the first child node of the specified node
  • node.lastchild - the last child node of the specific node
  • node.parentNode - a reference to the parent element of the specified node
  • node.nextSibling - the next node in the document that has the same parent as the specified node
  • node.previousSibling - the previous element that's on the same level as the specified node

If any of these properties do not exist for a specific node (eg, the last node of a parent will not have a next sibling), they will have a value of null .

Take a look at this simple page:

Example 5.8. access_element4.html (excerpt)   
 
<div id="outerGalaxy">  
 <ul id="starList">  
   <li id="star1">  
     Rigel  
   </li>  
   <li id="star2">  
     Altair  
   </li>  
   <li id="star3">  
     Betelgeuse  
   </li>  
 </ul>  
</div>

The list item with ID star2 could be referenced using any of these expressions:

/document.getElementById("star1").nextSibling;   
document.getElementById("star3").previousSibling;  
document.getElementById("starList").childNodes[1];  
document.getElementById("star1").parentNode.childNodes[1];

Whitespace Nodes

Some browsers will create whitespace nodes between the element nodes in any DOM structure that was interpreted from a text string (eg, an HTML file). Whitespace nodes are text nodes that contain only whitespace (tabs, spaces, new lines) to help format the code in the way it was written in the source file.

When you're traversing the DOM node by node using the above properties, you should always allow for these whitespace nodes. Usually, this means checking that the node you've retrieved is an element node, not just a whitespace node that's separating elements.

There are two easy ways to check whether a node is an element node or a text node. The nodeName property of a text node will always be " #text ", whereas the nodeName of an element node will identify the element type. However, in distinguishing text nodes from element nodes, it's easier to check the nodeType property. Element nodes have a nodeType of 1, whereas text nodes have a nodeType of 3. You can use this knowledge as a test when retrieving elements:

Example 5.9. access_element4.js (excerpt)

var star2 = document.getElementById("star1").nextSibling;

while (star2.nodeType == "3")
{
star2 = star2.nextSibling;
}

Using these DOM properties, it's possible to start your journey at the root html element, and end up buried in the legend of some deeply-nested fieldset?it's all just a matter of following the nodes.

Creating Elements and Text Nodes

JavaScript doesn't just have the ability to modify existing elements in the DOM; it can also create new elements and place them anywhere within a page's structure.

Solution

createElement
Example 5.10. create_elements.js (excerpt)   
 
var newAnchor = document.createElement("a");

The variable newAnchor will be a new a element, ready to be inserted into the page.

Specifying Namespaces in Documents with an XML MIME Type

If you're coding JavaScript for use in documents with a MIME type of application/xhtml+xml (or some other XML MIME type), you should use the method createElementNS , instead of createElement , to specify the namespace for which you're creating the element:

var newAnchor = document.createElementNS(   
   "http://www.w3.org/1999/xhtml", "a");

This distinction applies to a number of DOM methods, such as removeElement / removeElementNS and getAttribute / getAttributeNS ; however, we won't use the namespace-enhanced versions of these methods in this book.

Simon Willison provides a brief explanation of working with JavaScript and different MIME types on his web site.

The text that goes inside an element is actually a child text node of the element, so it must be created separately. Text nodes are different from element nodes, so they have their own creation method, createTextNode :

Example 5.11. create_elements.js (excerpt)   
 
var anchorText = document.createTextNode("monoceros");

If you're modifying an existing text node, you can access the text it contains via the nodeValue property. This allows you to get and set the text inside a text node:

var textNode = document.createTextNode("monoceros");   
var oldText = textNode.nodeValue;  
textNode.nodeValue = "pyxis";

The value of the variable oldText is now "monoceros" , and the text inside textNode is now "pyxis" .

You can insert either an element node or a text node as the last child of an existing element using its appendChild method. This method will place the new node after all of the element's existing children.

Consider this fragment of HTML:

Example 5.12. create_elements.html (excerpt)   
 
<p id="starLinks">  
 <a href="sirius.html">Sirius</a>  
</p>

We can use DOM methods to create and insert another link at the end of the paragraph:

Example 5.13. create_elements.js (excerpt)   
 
var anchorText = document.createTextNode("monoceros");  
 
var newAnchor = document.createElement("a");  
newAnchor.appendChild(anchorText);  
 
var parent = document.getElementById("starLinks");  
var newChild = parent.appendChild(newAnchor);

The value of the variable newChild will be a reference to the newly inserted element.

If we were to translate the state of the DOM after this code had executed into HTML code, it would look like this:

<p id="starLinks">   
 <a href="sirius.htm">Sirius</a><a>monoceros</a>  
</p>

We didn't specify any attributes for the new element, so it doesn't link anywhere at the moment. The process for specifying attributes is explained shortly in the section called "Reading and Writing the Attributes of an Element".

Discussion

There are three basic ways by which a new element or text node can be inserted into a web page. The approach you use will depend upon the point at which you want the new node to be inserted: as the last child of an element, before another node, or as the replacement for a node. The process of appending an element as the last child was explained above. You can insert the node before an existing node using the insertBefore method of its parent element, and you can replace a node using the replaceChild method of its parent element.

In order to use insertBefore, you need to have references to the node you're going to insert, and to the node before which you wish to insert it. Consider this HTML code:

Example 5.14. create_elements2.html (excerpt)   
 
<p id="starLinks">  
 <a id="sirius" href="sirius.html">Sirius</a>  
</p>

We can insert a new link before the existing one by calling insertBefore from its parent element (the paragraph):

Example 5.15. create_elements2.js (excerpt)   
 
var anchorText = document.createTextNode("monoceros");  
 
var newAnchor = document.createElement("a");  
newAnchor.appendChild(anchorText);  
 
var existingAnchor = document.getElementById("sirius");  
var parent = existingAnchor.parentNode;  
var newChild = parent.insertBefore(newAnchor, existingAnchor);

The value of the variable newChild will be a reference to the newly inserted element.

If we were to translate into HTML the state of the DOM after this operation, it would look like this:

<p id="starLinks">   
 <a>monoceros</a><a id="sirius" href="sirius.htm">Sirius</a>  
</p>

Instead, we could replace the existing link entirely using replaceChild :

Example 5.16. create_elements3.js (excerpt)   
 
var anchorText = document.createTextNode("monoceros");  
 
var newAnchor = document.createElement("a");  
newAnchor.appendChild(anchorText);  
 
var existingAnchor = document.getElementById("sirius");  
var parent = existingAnchor.parentNode;  
var newChild = parent.replaceChild(newAnchor, existingAnchor);

The DOM would then look like this:

<p id="starLinks">   
 <a>monoceros</a>  
</p>

Changing the Type of an Element

Are your ordered lists feeling a bit unordered? Do your headings have paragraph envy? Using a little JavaScript knowledge, it's possible to change the type of an element entirely, while preserving the structure of its children.

Solution

There's no straightforward, simple way to change the type of an element. In order to achieve this feat you'll have to perform a bit of a juggling act.

Let's assume that we want to change this paragraph into a div :

Example 5.17. change_type_of_element.js (excerpt)    
   
<p id="starLinks">    
 <a href="sirius.html">Sirius</a>    
 <a href="achanar.html">Achanar</a>    
 <a href="hadar.html">Hadar</a>    
</p>

We need to create a new div, move each of the paragraph's children into it, then swap the new element for the old:

Example 5.18. change_type_of_element.js (excerpt)    
   
var div = document.createElement("div");    
var paragraph = document.getElementById("starLinks");    
   
for (var i = 0; i < paragraph.childNodes.length; i++)    
{    
 var clone = paragraph.childNodes[i].cloneNode(true);    
   
 div.appendChild(clone);    
}    
   
paragraph.parentNode.replaceChild(div, paragraph);

The only unfamiliar line here should be the point at which a clone is created for each of the paragraph's children. The cloneNode method produces an identical copy of the node from which it's called. By passing this method the argument true, we indicate that we want all of that element's children to be copied along with the element itself. Using cloneNode , we can mirror the original element's children under the new div , then remove the paragraph once we're finished copying.

While cloning nodes is useful in some circumstances, it turns out that there's a cleaner way to approach this specific problem. We can simply move the child nodes of the existing paragraph into the new div . DOM nodes can belong only to one parent element at a time, so adding the nodes to the div also removes them from the paragraph:

Example 5.19. change_type_of_element2.js (excerpt)    
   
var div = document.createElement("div");    
var paragraph = document.getElementById("starLinks");    
   
while (paragraphNode.childNodes.length > 0){    
 div.appendChild(paragraphNode.firstChild);    
}    
   
paragraph.parentNode.replaceChild(div, paragraph);

Take Care Changing the Node Structure of the DOM

The elements in a collection are updated automatically whenever a change occurs in the DOM -- even if you copy that collection into a variable before the change occurs. So, if you remove from the DOM an element that was contained in a collection with which you had been working, the element reference will also be removed from the collection. This will change the length of the collection as well as the indexes of any elements that appear after the removed element.

When performing operations that affect the node structure of the DOM -- such as moving a node to a new parent element -- you have to be careful about iterative processes. The code above uses a while loop that only accesses the first child of the paragraph, because each time a child is relocated, the length of the childNodes collection will decrease by one, and all the elements in the collection will shift along. A for loop with a counter variable would not handle all the children correctly because it would assume that the contents of the collection would remain the same throughout the loop.

Discussion

There's no easy way to copy the attributes of an element to its replacement. (If you look at the DOM specification, it looks like there is. Unfortunately, Internet Explorer's support for the relevant properties and methods is just not up to the task.) If you want the new element to have the same id , class , href , and so on, you'll have to copy the values over manually:

Example 5.20. change_type_of_element.js (excerpt)    
   
div.id = paragraph.getAttribute("id");    
div.className = paragraph.className;
Removing an Element or Text Node

Once an element has outlived its usefulness, it's time to give it the chop. You can use JavaScript to remove any element cleanly from the DOM.

Solution

The removeChild method removes any child node from its parent, and returns a reference to the removed object.

Let's start off with this HTML:

Example 5.21. remove_element.html (excerpt)    
   
<p>    
 <a id="sirius" href="sirius.html">Sirius</a>    
</p>

We could use removeChild to remove the hyperlink from its parent paragraph like so:

Example 5.22. remove_element.js (excerpt)    
   
var anchor = document.getElementById("sirius");    
var parent = anchor.parentNode;    
var removedChild = parent.removeChild(anchor);

The variable removedChild will be a reference to the a element, but that element will not be located anywhere in the DOM: it will simply be available in memory, much as if we had just created it using createElement . This allows us to relocate it to another position on the page, it we wish, or we can simply let the variable disappear at the end of the script, and the reference will be lost altogether -- effectively deleting it. Following the above code, the DOM will end up like this:

<p>    
</p>

Of course, you don't need to assign the return value from removeChild to a variable. You can just execute it and forget about the element altogether:

var anchor = document.getElementById("sirius");    
var parent = anchor.parentNode;    
parent.removeChild(anchor);

Discussion

If the element that you're deleting has children that you wish to preserve (i.e., you just want to "unwrap" them by removing their parent), you must rescue those children to make sure they stay in the document when their parent is removed. You can achieve this using the already-mentioned insertBefore method, which, when used on elements that are already contained in the DOM, first removes them, then inserts them at the appropriate point.

The paragraph in the following HTML contains multiple children:

Example 5.23. remove_element2.html (excerpt)    
   
<div id="starContainer">    
 <p id="starLinks">    
   <a href="aldebaran.html">Aldebaran</a>    
   <a href="castor.html">Castor</a>    
   <a href="pollux.html">Pollux</a>    
 </p>    
</div>

We can loop through the paragraph's childNodes collection, and relocate each of its children individually before removing the element itself:

Example 5.24. remove_element2.js (excerpt)    
   
var parent = document.getElementById("starLinks");    
var container = document.getElementById("starContainer");    
   
while (parent.childNodes.length > 0)    
{    
 container.insertBefore(parent.childNodes[0], parent);    
}    
   
container.removeChild(parent);

The page's DOM will now look like this:

<div id="starContainer">    
 <a href="aldebaran.htm">Aldebaran</a>    
 <a href="castor.htm">Castor</a>    
 <a href="pollux.htm">Pollux</a>    
</div>
Reading and Writing the Attributes of an Element

The most frequently used parts of an HTML element are its attributes?its id, class, href, title, or any of a hundred other pieces of information that can be included in an HTML tag. JavaScript is able not only to read these values, but write them as well.

Solution

Two methods exist for reading and writing an element's attributes. getAttribute allows you to read the value of an attribute, while setAttribute allows you to write it.

Consider this HTML:

Example 5.25. read_write_attributes.html (excerpt)    
   
<a id="antares" href="antares.html" title="A far away place">    
 Antares</a>

We would be able to read the attributes of the element like so:

Example 5.26. read_write_attributes.js (excerpt)    
   
var anchor = document.getElementById("antares");    
var anchorId = anchor.getAttribute("id");    
var anchorTitle = anchor.getAttribute("title");

The value of the variable anchorId will be "antares" , and the value of the variable anchorTitle will be "A far away place" .

To change the attributes of the hyperlink, we use setAttribute , passing it the name of the attribute to be changed, and the value we want to change it to:

Example 5.27. read_write_attributes2.js (excerpt)    
   
var anchor = document.getElementById("antares");    
   
anchor.setAttribute("title", "Not that far away");    
   
var newTitle = anchor.getAttribute("title");

The value of the variable newTitle will now be "Not that far away" .

Discussion

In its journey from the free-roaming Netscape wilderness to the more tightly defined, standards-based terrain of the modern age, the DOM standard has picked up a fair amount of extra syntax for dealing with HTML. One of the most pervasive of these extras is the mapping between DOM properties and HTML attributes.

When a document is parsed into its DOM form, special attribute nodes are created for an element's attributes. These nodes are not accessible as "children" of that element: they are accessible only via the two methods mentioned above. However, as a throwback to the original DOM implementations (called DOM 0, where the zero suggests these features came prior to standards), current DOM specs contain additional functionality that's specific to HTML. In particular, attributes are accessible directly as properties of an element. So, the href attribute of a hyperlink is accessible through link.getAttribute("href") as well as through link.href .

This shortcut syntax is not only cleaner and more readable: in some situations it is also necessary. Internet Explorer 6 and versions below will not propagate changes made via setAttribute to the visual display of an element. So any changes that are made to the class , id , or style of an element using setAttribute will not affect the way it's displayed. In order for those changes to take effect, they must be made via the element node's attribute-specific properties.

To further confuse matters, the values that are returned when an attribute-specific property is read vary between browsers, the most notable variations occurring in Konqueror. If an attribute doesn't exist, Konqueror will return null as the value of an attribute-specific property, while all other browsers will return an empty string. In a more specific case, some browsers will return link.getAttribute("href") as an absolute URL (e.g., "http://www.example.com/antares.html" ), while others return the actual attribute value (e.g., "antares.html" ). In this case, it's safer to use the dot property, as it consistently returns the absolute URL across browsers.

So, what's the general solution to these problems?

The basic rule is this: if you are certain that an attribute has been assigned a value, it's safe to use the dot property method to access it. If you're unsure whether or not an attribute has been set, you should first use one of the DOM methods to ensure that it has a value, then use the dot property to obtain its value.

For reading an unverified attribute, use the following:

var anchor = document.getElementById("sirius");    
   
if (anchor.getAttribute("title") &&    
   anchor.title == "Not the satellite radio")    
{    
 ...    
}

This makes sure that the attribute exists, and is not null , before fetching its value.

For writing to an unverified attribute, use the following code:

var anchor = document.getElementById("sirius");    
   
anchor.setAttribute("title", "");    
anchor.title = "Yes, the satellite radio";

This code makes sure that the attribute is created correctly first, and is then set in such a way that Internet Explorer will not have problems if the attribute affects the visual display of the element.

This rule has a few exceptions for attributes whose existence you can guarantee. The most notable of these "must-have" attributes are style and class, which will always be valid for any given element; thus, you can immediately reference them as dot properties ( element.style and element.className respectively).

 class is one of two attributes that get a little tricky, because class is a reserved word in JavaScript. As a property, it is written element.className , but using getAttribute / setAttribute , we write element.getAttribute("class") , except in Internet Explorer, where we still use element.getAttribute("className") .

The other attribute that we have to watch out for is the for attribute of a label. It follows the same rules as class, but its property form is htmlFor . Using getAttribute / setAttribute , we write element.getAttribute("for") , but in Internet Explorer it's element.getAttribute("htmlFor") .

Getting all Elements with a Particular Attribute Value

The ability to find all the elements that have a particular attribute can be pretty handy when you need to modify all elements that have the same class or title, for example.

Solution

In order to find elements with a particular attribute value, we need to check every element on the page for that attribute. This is a very calculation-intensive operation, so it shouldn't be undertaken lightly. If you wanted to find all input elements with type="checkbox" , you're better off limiting your search to input elements first:

var inputs = document.getElementsByTagName("input");    
   
for (var i = 0; i < inputs.length; i++)    
{    
 if (inputs.getAttribute("type") == "checkbox")    
 {    
   ...    
 }    
}

This will require less calculation than iterating through every element on the page and checking its type. However, the function presented in this solution -- getElementsByAttribute -- is ideal when you need to find a number of elements of different types that have the same attribute value.

The easiest way to check every element on a page is to loop through the collection returned by getElementsByTagName("*") . The only problem with this method is that Internet Explorer 5.0 and 5.5 do not support the asterisk wildcard for tag selection. Luckily, these browsers support the document.all property, which is an array containing all the elements on the page. getElementsByAttribute handles this issue with a simple code branch, then proceeds to check the elements for a given attribute value, adding matches to an array to be returned:

Example 5.28. get_elements_by_attribute.js (excerpt)    
   
function getElementsByAttribute(attribute, attributeValue)    
{    
 var elementArray = new Array();    
 var matchedArray = new Array();    
   
 if (document.all)    
 {    
   elementArray = document.all;    
 }    
 else    
 {    
   elementArray = document.getElementsByTagName("*");    
 }    
   
 for (var i = 0; i < elementArray.length; i++)    
 {    
   if (attribute == "class")    
   {    
     var pattern = new RegExp("(^| )" +    
         attributeValue + "( |$)");    
   
     if (pattern.test(elementArray[i].className))    
     {    
       matchedArray[matchedArray.length] = elementArray[i];    
     }    
   }    
   else if (attribute == "for")    
   {    
     if (elementArray[i].getAttribute("htmlFor") ||    
         elementArray[i].getAttribute("for"))    
     {    
       if (elementArray[i].htmlFor == attributeValue)    
       {    
         matchedArray[matchedArray.length] = elementArray[i];    
       }    
     }    
   }    
   else if (elementArray[i].getAttribute(attribute) ==    
       attributeValue)    
   {    
     matchedArray[matchedArray.length] = elementArray[i];    
   }    
 }    
   
 return matchedArray;    
}

A lot of the code in getElementsByAttribute deals with the browser differences in attribute handling that were mentioned earlier in this chapter, in the section called "Reading and Writing the Attributes of an Element". The necessary techniques are used if the required attribute is class or for. As an added bonus when checking for a match on the class attribute, if an element has been assigned multiple classes, the function automatically checks each of these to see whether it matches the required value.

Adding and Removing Multiple Classes to/from an Element

Combining multiple classes is a very useful CSS technique. It provides a very primitive means of inheritance by allowing a number of different styles to be combined on the one element, allowing you to mix and match different effects throughout a site. They're particularly useful in situations like highlighting elements: a class can be added that highlights an element without disturbing any of the other visual properties that may have been applied to the element by other classes. However, if you are assigning classes in JavaScript you have to be careful that you don't inadvertently overwrite previously assigned classes.

Solution

The class for any element is accessible via its className property. This property allows you both to read and write the classes that are currently applied to that element. Because it's just one string, the most difficult part of working with className is that you need to deal with the syntax it uses to represent multiple classes.

The class names in an element's className property are separated by spaces. The first class name is not preceded by anything, and the last class name is not followed by anything. This makes it easy to add a class to the class list naively: just concatenate a space and the new class name to the end of className . However, you'll want to avoid adding a class name that already exists in the list, as this will make removing the class harder. You'll also want to avoid using a space at the beginning of the className value, because this will cause errors in Opera 7:

Example 5.29. add_remove_classes.js (excerpt)    
   
function addClass(target, classValue)    
{    
 var pattern = new RegExp("(^| )" + classValue + "( |$)");    
   
 if (!pattern.test(target.className))    
 {    
   if (target.className == "")    
   {    
     target.className = classValue;    
   }    
   else    
   {    
     target.className += " " + classValue;    
   }    
 }    
   
 return true;    
}

First, addClass creates a regular expression pattern containing the class to be added. It then uses this pattern to test the current className value. If the class name doesn't already exist, we check for an empty className value (in which case the class name is assigned to the property verbatim), or we append to the existing value a space and the new class name.

Separating Classes

Some regular expression examples for finding classes use the word boundary special character ( b ) to separate classes. However, this will not work with all valid class names, such as those containing hyphens.

The process for removing a class uses a regular expression pattern that's identical to the one we use to add a class, but we don't need to perform as many checks:

Example 5.30. add_remove_classes.js (excerpt)    
   
function removeClass(target, classValue)    
{    
 var removedClass = target.className;    
 var pattern = new RegExp("(^| )" + classValue + "( |$)");    
   
 removedClass = removedClass.replace(pattern, "$1");    
 removedClass = removedClass.replace(/ $/, "");    
   
 target.className = removedClass;    
   
 return true;    
}

After removeClass has executed the replacement regular expression on a copy of the className property's value, it cleans up the resulting value by removing any trailing space (which is created when we remove the last class in a multiple class className ), then assigns it back to the target's className .

Summary

This chapter introduced the basic but powerful tools that you'll need in order to manipulate the Document Object Model. It's important that you understand the DOM -- the skeleton beneath everything you see in a browser -- as you manipulate any web page. Knowing how to create, edit, and delete parts of the DOM is crucial to understanding the remainder of this book. Once you've mastered these techniques, you'll be well on your way to becoming a proficient JavaScript programmer.

Chapter 7. Working with Windows and Frames

This chapter is about simple window and frame manipulation, including tasks like opening popups, communicating between frames, (The techniques involved in reading data from an iframe will be covered in Chapter 18, Building Web Applications with JavaScript.) and finding out the page's scrolling position.

Plenty of people feel that window manipulation is akin to the Dark Side. They believe that a window is part of the user's GUI, not the document, and since JavaScript is a document scripting language, it has no business manipulating windows.

I'm generally inclined to agree, yet I know that opinion is sometimes a luxury. If your clients ask for something specific, you can't necessarily change their minds, or have the freedom to turn down work on the basis of such a principle. In this chapter, we'll cover a range of practical window and frame manipulation tasks while remaining sensitive to the usability and accessibility issues that can arise from their use.

Note, though, that there are limits, and some varieties of window scripting are particularly unfriendly. We won't be dealing with aggressive tactics like closing or modifying the user's primary window, moving windows around the screen, or opening full-screen or "chromeless" windows. These are exactly the kinds of abuses that have given JavaScript a bad name.

Through most of this chapter we'll be looking closely at the properties and methods of the window object. These are implemented by different browsers in a variety of ways, most of which have been in use since the days before JavaScript was standardized.

We'll have quite a few code branches to deal with, but we'll avoid the dreaded browser sniffing by careful use of object detection, the process of detecting an object or feature to test for compatibility, rather than detecting specific browsers.

Using Popup Windows

Should you use popup windows? The most considered answer I have is this: not if you can help it. Popup windows have gained a bad reputation from marketers' aggressive use of them, but even requested popups can be barriers to good usability.

I won't say that popups are never appropriate, but I will say that they're seldom so. Nevertheless, there are situations where popping open a new window is arguably the most appropriate solution: an online survey might be one example, as the format may make the content more approachable; DHTML games are another, as the viewport may need to be of a known size.

I'll qualify my opinion by discussing the problems that popups create, then providing a pragmatic method for using them that mitigates these problems as much as possible.

What's Wrong with Popups?

The main problem with most popup window scripts is that they don't consider the needs of the user?they address only the needs of the designer. The results? We've all seen them:

  • popups that are generated from links, though those links do nothing when scripting is not available
  • popup windows that don't have a status bar, so you can't necessarily tell whether the document has loaded or stalled, is still loading, etc.
  • popups that don't give users the ability to resize the window, and popups that fail to generate scrollbars for content that might scale outside the window
  • windows that are "chromeless," or open to the full size of the user's screen

These issues are not just questions of usability, but of accessibility as well. For example, screen-reader users may not be notified by their devices that a new window has opened. This could obviously cause confusion if they then attempted to go back in the browser history (they can't). The same thing might happen for a sighted user if a window opens at full-size: you and I may be familiar with using the taskbar to monitor open windows, but not all computer users are -- they may not even realize that a new window has popped up.

If you're going to use popups, looking out for issues like these, and being generally sensitive to their impacts, will make your popups friendlier to users, and less of a strain on your conscience.

Also, bear in mind that, from a developer's perspective, popup windows are not guaranteed to work: most browsers now include options to suppress popup windows, and in some cases, suppression occurs even if the popup is generated in response to a user event.

You may be able to allow for this as you would for situations in which scripting was not supported: by ensuring that the underlying trigger for the popup still does something useful if the popup fails. Or you might have your code open a window and then check its own closed property, to see if it's actually displayed (we'll look at this technique in the next solution).

But neither of these approaches is guaranteed to work with every browser and popup blocker out there, so for this as much as the usability reasons, it's simpler and better to avoid using popups whenever you can.

How Do I Minimize the Problems?

What we need to do is establish some golden rules for the ethical use of popups:

  • Make sure any triggering link degrades properly when scripting is not available.
  • Always include the status bar.
  • Always include a mechanism to overflow the content: either allow window resizing, or allow scrollbars to appear, or both.
  • Don't open windows that are larger than 640x480 pixels. By limiting the size of popups, you ensure that they're smaller than users' primary windows on the vast majority of monitors. This increases the likelihood that the user will realize that the popup is a new window.

Solution

Here's a generic popup function that's based on the guidelines above:

Example 7.1. make-popup.js (excerpt)     
   
function makePopup(url, width, height, overflow)    
{    
 if (width > 640) { width = 640; }    
 if (height > 480) { height = 480; }    
   
 if (overflow == '' || !/^(scroll|resize|both)$/.test(overflow))    
 {    
   overflow = 'both';    
 }    
   
 var win = window.open(url, '',    
     'width=' + width + ',height=' + height    
     + ',scrollbars=' + (/^(scroll|both)$/.test(overflow) ?    
     'yes' : 'no')    
     + ',resizable=' + (/^(resize|both)$/.test(overflow) ?    
     'yes' : 'no')    
     + ',status=yes,toolbar=no,menubar=no,location=no'    
 );    
   
 return win;    
}

As well as limiting the window size, this script refuses to create a popup that doesn't have an overflow, so if you don't specify "scroll" , "resize" , or "both" for the overflow argument, the default setting of "both" will be used.

The Ternary Operator

This script uses a shortcut expression called a ternary operator to evaluate each of the overflow options. The ternary operator uses ? and : characters to divide the two possible outcomes of an evaluation, and is equivalent to a single pair of if..else conditions. Consider this code:

if (outlook == 'optimistic') { glass = 'half-full'; }     
else { glass = 'half-empty'; }

That code is equivalent to the markup below:

glass = (outlook == 'optimistic' ? 'half-full' :     
   'half-empty');

The parentheses are not required, but you may find they make the expression easier to read. For more about this and other useful shortcuts, see Chapter 20, Keeping up the Pace.

Once you have the popup function in place, you can call it in a variety of ways. For example, you could use a regular link:

Example 7.2. make-popup.html (excerpt)     
   
<a href="survey.html" id="survey_link">Online survey</a>

If scripting is not available, this will work just like any other link, but if scripting is available, the script can trigger a click event handler that passes its href to the makePopup function, along with the other settings. The return value of the handler depends on whether or not the window is actually opened; browsers that block the popup will follow the link as normal:

Example 7.3. make-popup.js (excerpt)     
   
document.getElementById('survey_link').onclick = function()    
{    
 var survey = makePopup(this.href, 640, 480, 'scroll');    
   
 return survey.closed;    
};

In general, if you have a script that requires that a window be generated, you can call the makePopup function directly with a URL:

var cpanel = makePopup('cpanel.html', 480, 240, 'resize');

If you need to close that window later in your script, you can do so by using the close method on the stored window reference:

cpanel.close();

Discussion.

The window.open method can take a number of arguments -- in addition to the URL and window name -- which specify whether the window should have particular decorations, such as the menu bar, tool bar, or address (location) bar. These arguments are passed as a comma-delimited string to the third argument of window.open :

var win = window.open('page.html', 'winName',     
   'width=640,height=480,'    
   + 'scrollbars=yes,resizable=yes,status=yes,'    
   + 'toolbar=no,menubar=no,location=no');

In our makePopup function, the menubar , toolbar , and location arguments are all preset to no because these elements are rarely useful for popup windows -- they're navigational tools, after all. Popups are mostly used for one-page interfaces, or those in which history navigation is discouraged, such as our survey example, or the logon procedure for a bank's web site.

You can change those arguments if you need to, but the status argument should always be set to yes, because turning it off undermines good usability. (I know -- I've mentioned it already, but I'm saying it again because it's important!)

The resizable argument may not have any effect -- in some browsers, either by design or as a result of user preferences, it's not possible to create non-resizable windows, even if you set this value to no. In fact, in Opera 8 for Mac OS X, it's not possible to create custom-sized windows at all -- a created window will appear as a new tab in the current window. That specific exception might not be significant in itself, but it serves to illustrate the general point that control over the properties of a created window is not absolutely guaranteed.

Once a new window is open, you can bring it into focus using the object's focus method. This isn't usually necessary -- generally, it happens by default -- but the technique may be useful when you're scripting with multiple windows:

var cpanel = makePopup('cpanel.html', 480, 240, 'resize');     
cpanel.focus();

Alternatively, you may want to open a popup but keep the focus in the primary window (thereby creating a so-called "popunder"). You can take the focus away from a window using its blur method:

var cpanel = makePopup('cpanel.html', 480, 240, 'resize');     
cpanel.blur();

However, in that case you can't predict where the focus will go to next, so it's more reliable to refocus the primary window:

var cpanel = makePopup('cpanel.html', 480, 240, 'resize');     
self.focus();
Opening Off-site Links in a New Window

In the strict versions of HTML 4 and XHTML 1, the target attribute for links no longer exists. One interpretation of this is that web pages simply shouldn't open links in new windows; another is that targeting doesn't have universal semantics and therefore shouldn't be defined in HTML. (The CSS 3 working draft includes a set of target properties for link presentation , which could eventually see this mechanism handed to CSS instead. Personally, I hope this never gets past the draft stage, because it's nothing to do with CSS: interface control is no more appropriate in a design language than it is in a semantic markup language!)

There are other interpretations, and the arguments are long (and sometimes tedious), but suffice it to say that you may find yourself needing a solution to this problem. Whatever your personal views may be, it's a common request of web development clients.

Solution

This script identifies links by the rel attribute value external. The rel attribute is a way of describing the relationship between a link and its target, so its use for identifying links that point to another site is semantically non-dubious:

Example 7.4. offsite-links.html (excerpt)     
   
<a href="http://www.google.com/" rel="external">Google    
 (offsite)</a>

If each external link is identified like that, a single document.onclick event handler can process clicks on all such links:

Example 7.5. offsite-links.js     
   
document.onclick = function(e)    
{    
 var target = e ? e.target : window.event.srcElement;    
   
 while (target && !/^(a|body)$/i.test(target.nodeName))    
 {    
   target = target.parentNode;    
 }    
   
 if (target && target.getAttribute('rel')    
     && target.rel == 'external')    
 {    
   var external = window.open(target.href);    
   
   return external.closed;    
 }    
}

Discussion

Using a single, document-wide event handler is the most efficient approach -- it's much better than iterating through all the links and binding a handler to each one individually. We can find out which element was actually clicked by referencing the event target property. For more about events and event properties, see Chapter 13, Basic Dynamic HTML, but here's a brief summary of the situation.

Two completely different event models are employed by current browsers. The script establishes which one should be used by looking for e -- the event argument that's used by Mozilla browsers, and has been adopted by most other browsers -- as opposed to the window.event object used by Internet Explorer. It then saves the object property that's appropriate to the model in use: either target for Mozilla and like browsers, or srcElement for IE.

The target object (if it's not null ) can be one of three things: a link element node, an element or text node inside a link, or some other node. We want the first two cases to be handled by our script, but clicks arising from the last situation may be safely ignored. What we do is follow the trail of parent nodes from the event target until we either find a link, or get to the body element.

Once we have a unified target link, we need simply to check for a rel attribute with the correct value; if it exists, we can open a window with the link's href , and if all of that is successful (as judged by the new window object's closed property), the handler will return false, preventing the original link from being followed.

Passing a link to window.open without defining arguments will create a window with default decorations -- as will a link with target="_blank" .

The First Test

We use getAttribute as the first test for rel because attribute-specific properties are only reliable if you know for certain that the attribute in question has been assigned a value. We can't go straight to testing target.rel against a string, because it might be null or undefined. This was discussed in more detail in the section called "Reading and Writing the Attributes of an Element".

Communicating Between Frames

If you're working in a framed environment, it may be necessary to have scripts communicate between frames, either reading or writing properties, or calling functions in different documents.

If you have a choice about whether or not to use frames, I'd strongly advise against doing so, because they have many serious usability and accessibility problems, quite apart from the fact that they're conceptually broken (they create within the browser states that cannot be addressed). But as with your use of popups, in some cases you may not have a choice about your use of frames. So if you really must use them, here's what you'll need to do.

Solution

Let's begin with a simple frameset document:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN"      
   "http://www.w3.org/TR/html4/frameset.dtd">      
<html>      
 <head>      
   <title>A frameset document</title>      
 </head>      
 <frameset cols="200, *">      
   <frame src="navigation.html" name="navigationFrame">      
   <frame src="content.html" name="contentFrame">      
   <noframes>      
     <p>This frameset document contains:</p>      
     <ul>      
       <li><a href="navigation.html">Site navigation</a></li>      
       <li><a href="contents.html">Main content</a></li>      
     </ul>      
   </noframes>      
 </frameset>      
</html>

We can use four references for cross-frame scripting:

  • window or self refers to the current framed page.
  • parent refers to the page that contains the frame that contains the current page.
  • top refers to the page at the very top of the hierarchy of frames, which will be the same as parent if there's only one frameset in the hierarchy.
  • The frames collection is an associative array of all the frames in the current page.

Let's say we have a script in contentFrame that wants to communicate the page in navigationFrame . Both pages are contained in a single frameset -- the only one in the hierarchy -- so we could successfully make any of the following references from within contentFrame :

  • parent.frames[0]
  • top.frames[0]
  • parent.frames['navigationFrame']
  • top.frames['navigationFrame']

The frames collection is an associative array (like the forms collection we saw in Chapter 6, Processing and Validating Forms), so each element can be accessed by either index or name. It's generally best to use the name (unless you have a good reason not to) so that you won't have to edit your code later if the frame order changes. By the same token, parent references in a complex nested frameset can change if the hierarchy changes, so I generally recommend that developers always start referencing from top. Of the above options, the reference I prefer, then, is top.frames['navigationFrame'] .

Now that we have a reference to the frame, we can call a function in the other framed page:

Example 7.6. frames-navigation.js (excerpt)      
     
var navframe = top.frames['navigationFrame'];      
navframe.callMyFunction();

Alternatively, we can get a reference to the other framed document, and work with the DOM from there:

Example 7.7. frames-navigation.js (excerpt)      
     
var navdoc = navframe.document;      
var menu = navdoc.getElementById('menulist');

Discussion

Communication between frames is only allowed for documents in the same domain -- for security reasons, it's not possible to work with a document that was loaded from a different domain than the script. It wouldn't do, for example, for a malicious site owner to load a site that you visit regularly into a frame, and steal the personal data you enter there.

In fact, some browsers let users disallow all scripts from communicating between frames, just to eradicate any possibility of a cross-site scripting vulnerability, and there's no way to work around this preference if your script finds itself running in a browser so configured.

If you do have users who are complaining of problems (and they can't or won't change their settings to allow cross-frame scripting), the safest thing to do is simply to avoid cross-frame scripting altogether.

Alternative methods of passing data between pages are discussed in Chapter 6, Processing and Validating Forms and Chapter 8, Working with Cookies.

Getting the Scrolling Position

Page scrolling is one of the least-standardized properties in JavaScript: three variations are now in use by different versions of different browsers. But with a few careful object tests, we can reliably get a consistent value.

Solution

There are three ways of getting this information. We'll use object tests on each approach, to determine the level of support available:

Example 7.8. get-scrolling-position.js (excerpt)      
     
function getScrollingPosition()      
{      
 var position = [0, 0];      
     
 if (typeof window.pageYOffset != 'undefined')      
 {      
   position = [      
       window.pageXOffset,      
       window.pageYOffset      
   ];      
 }      
     
 else if (typeof document.documentElement.scrollTop      
     != 'undefined' && document.documentElement.scrollTop > 0)      
 {      
   position = [      
       document.documentElement.scrollLeft,      
       document.documentElement.scrollTop      
   ];      
 }      
     
 else if (typeof document.body.scrollTop != 'undefined')      
 {      
   position = [      
       document.body.scrollLeft,      
       document.body.scrollTop      
   ];      
 }      
     
 return position;      
}

The function can now be called as required. Here's a simple demonstration, using a window.onscroll event handler, that gets the figures and writes them to the title bar:

Example 7.9. get-scrolling-position.js (excerpt)      
     
window.onscroll = function()      
{      
 var scrollpos = getScrollingPosition();      
 document.title = 'left=' + scrollpos[0] + ' top=' +      
     scrollpos[1];      
};

The Problem with scroll

scroll is not the most reliable of events: it may not fire at all in Konqueror or Safari 1.0, or when the user navigates with a mouse wheel in Firefox. And if it does fire, it may do so continually and rapidly (as it does in Internet Explorer), which can be slow and inefficient if the scripting you set to respond to the event is very complex.

If you have difficulties of this kind, you may find it better to use the setInterval function instead of an onscroll event handler. setInterval will allow you to call the function at a predictable interval, rather than in response to an event. You can find out more about this kind of scripting in Chapter 14, Time and Motion, but here's a comparable example:

window.setInterval(function()
{
var scrollpos = getScrollingPosition();
document.title = 'left=' + scrollpos[0] + ' top=' +
scrollpos[1];
}, 250);

Discussion

The only real complication here is that IE 5 actually does recognize the documentElement.scrollTop property, but its value is always zero, so we have to check the value as well as looking for the existence of the property.

Otherwise, it doesn't really matter to us which browser is using which property; all that matters is that our script gets through one of the compatibility tests and returns a useful value. However, the properties used by each browser are shown here for reference:

  • window.pageYOffset is used by Firefox and other Mozilla browsers, Safari, Konqueror, and Opera.
  • document.documentElement.scrollTop is used by IE 6 in standards-compliant mode.
  • document.body.scrollTop is used by IE 5, and IE 6 in "Quirks" mode.

This list doesn't tell the complete story, but it's intended primarily to describe the ordering of the tests. More recent Mozilla browsers (such as Firefox) also support documentElement.scrollTop and body.scrollTop , by the same rendering mode rules as IE 6. Safari and Konqueror support body.scrollTop in either mode. Opera supports all three properties in any mode!

But none of this is important for you to know -- browser vendors add these multiple properties to allow for scripts that are unaware of one property or another, not to provide arbitrary choices for the sake of it. From our perspective, the important point is to settle on a set of compatibility tests that ensures our script will work as widely as possible.

Rendering Modes

"Standards" mode and "Quirks" mode are the two main rendering modes in use by current browsers. These modes affect various aspects of the output document, including which element is the canvas ( <body> or <html> ), and how CSS box sizes are calculated. For more on rendering modes, see Chapter 11, Detecting Browser Differences.

Making the Page Scroll to a Particular Position

All current browsers implement the same (nonstandard) methods for scrolling a page. At least something here is simple!

Solution

There are two methods that can be used to scroll the page (or rather, the window or frame), either by a particular amount ( window.scrollBy ), or to a particular point ( window.scrollTo ):

Example 7.10. scroll-page.js (excerpt)      
     
//scroll down 200 pixels      
window.scrollBy(0, 200);      
     
Example 7.11. scroll-page.js (excerpt)      
     
//scroll across 200 pixels      
window.scrollBy(200, 0);      
     
Example 7.12. scroll-page.js (excerpt)      
     
//scroll to 300 from the edge and 100 from the top      
window.scrollTo(300, 100);      
     
Example 7.13. scroll-page.js (excerpt)      
     
//scroll to the beginning      
window.scrollTo(0, 0);

These examples say: scroll down by 200 pixels, then across by 200 pixels, then to a point that's 300 pixels from the left and 100 pixels from the top, then back to the top corner.

Getting the Viewport Size (the Available Space inside the Window)

The details of the viewport size are needed for many kinds of scripting, wherever available space is a factor in the script's logic. This solution provides a utility function for getting the viewport size We'll be seeing the function again quite a few times throughout this book!

Solution

The properties we need are implemented in three different ways, like the properties we saw for page scrolling in the previous section (the section called "Making the Page Scroll to a Particular Position"). As was the case in that example, we can use object testing to determine which implementation is relevant, including the test for a zero-value that we need in IE 5 (this test is required for the same reason: because, though the property exists, it isn't what we want):

Example 7.14. get-viewport-size.js (excerpt)      
     
function getViewportSize()      
{      
 var size = [0, 0];      
     
 if (typeof window.innerWidth != 'undefined')      
 {      
   size = [      
       window.innerWidth,      
       window.innerHeight      
   ];      
 }      
 else if (typeof document.documentElement != 'undefined'      
     && typeof document.documentElement.clientWidth !=      
     'undefined' && document.documentElement.clientWidth != 0)      
 {      
   size = [      
       document.documentElement.clientWidth,      
       document.documentElement.clientHeight      
   ];      
 }      
 else      
 {      
   size = [      
       document.getElementsByTagName('body')[0].clientWidth,      
       document.getElementsByTagName('body')[0].clientHeight      
   ];      
 }      
     
 return size;      
}

The function returns an array of the width and height, so we can call it whenever we need that data:

Example 7.15. get-viewport-size.js (excerpt)      
     
window.onresize = function()      
{      
 var size = getViewportSize();      
 alert('Viewport size: [' + size[0] + ', ' + size[1] + ']');      
};
摘要

We've covered the basics of window and frame manipulation from a pragmatist's point of view in this chapter. We've also talked about principles and techniques that we can use to ensure that scripts like this are as user-friendly and as accessible as we can make them. Doubtless, this kind of work will remain controversial, and clearly we do need some kind of targeting mechanism, because even though the use of frames is slowly dying out, the advent of ever more sophisticated interfaces keeps these issues alive.

I rather like the XLink standard's show attribute, which has values like new and replace . These suggest a target process (open a new window, and replace the contents of the current window, respectively) but they don't actually define specific behaviors. They leave it up to the user agent to control what actually happens, so, for example, new could be used to open tabs instead of windows.

Chapter 13. Basic Dynamic HTML

Dynamic HTML isn't a single piece of technology that you can point to and say, "This is DHTML." The term is a descriptor that encompasses all of the technologies that combine to make a web page dynamic: the technologies that let you create new elements without refreshing the page, change the color of those elements, and make them expand, contract, and zoom around the screen.

DHTML uses HTML, the DOM, and CSS in combination with a client-side scripting language -- JavaScript -- to bring life to what was traditionally a static medium. In previous chapters, we learned that we can use JavaScript to manipulate parts of a page to achieve some very handy results. DHTML provides solutions to much more complex problems by assembling these parts into a coherent whole -- one that satisfies real-world needs, rather than programming puzzles.

This chapter explores a few of the tools we need in order to create effective user interfaces with DHTML. It then discusses a couple of simple widgets in preparation for the more complex modules we'll consider throughout the rest of this book.

处理事件

Any interaction that users have with a web page -- whether they're moving the mouse or tapping the keyboard -- will cause the browser to generate an event. Sometimes, we want our code to respond to this interaction, so we listen for these events, which let us know when we should execute our code.

Solution

There are two ways to handle events: the short way, and the W3C way. Each has its pros and cons, but both allow you to execute a specified function when an event occurs on a particular element.

The Short Way: Using Event Handlers

The shorter way of handling an event is to use the DOM 0 event handlers that are assigned as shortcut properties of every element. Much as we saw in Chapter 5, Navigating the Document Object Model when we discussed DOM 0 attribute shortcuts, these event handlers are not future-proof. However, they do offer some advantages over standard W3C event listeners:

  • Every browser that's currently in operation supports DOM 0 event handlers without the need for code branching.
  • Each function executed by a DOM 0 event handler has access to the exact element to which the event handler was assigned. (As you'll see later, this is not always available in W3C event listeners.)

The main problem with utilizing DOM 0 event handlers is that they are not designed to work with multiple scripts. Every time you assign a DOM 0 event handler, you overwrite any previously assigned handler for that event. This can interfere with the operation of multiple scripts that require event handling on the same element. With W3C event listeners, you can apply any number of event listeners on the same element, and enjoy the ability to remove any of them at any time.

If you can be certain that your code will not interfere with someone else's event handling (eg, you're placing events on elements that are created dynamically in your own script), it will be safe to use DOM 0 event handlers. But -- all things being equal -- it is safer to use the W3C event listeners wherever practical, as we do in this book.

A number of DOM 0 event handlers are available via the browser; Table 13.1, "DOM 0 event handlers" lists the most commonly used handlers.

Table 13.1. DOM 0 event handlers
Table listing DOM 0 event handlers

In using DOM 0 event handlers, once you have a reference to the element whose events you want to handle, it's a simple matter of assigning a handling function to the appropriate property:

Example 13.1. handle_events.js (excerpt)       
     
var mylink = document.getElementById("mylink");      
     
mylink.onclick = engage;      
...      
function engage()      
{      
 alert("Engage!");      
     
 return false;      
}

You'll note that, in the function assignment ( button.onclick = engage; ), parentheses do not follow the function name. Their inclusion would execute the function immediately, and assign the return value as the event handler. By omitting the parentheses, you can assign the function itself to the handler. This also means that you cannot supply arguments directly to the handling function: the function must obtain its information through other means.

Anonymous Functions

Instead of supplying a reference to a named function, you can supply an anonymous function for an event handler:

var mylink = document.getElementById("mylink");

mylink.onclick = function()
{
alert("Engage!");

return false;
}

Depending on whether you need to reuse the handling function (and your own coding preferences), this can be an easier way of writing event handling code.

The return value of the handling function determines whether the default action for that event occurs. So, in the preceding code, if mybutton were a hyperlink, its default action when clicked would be to navigate to its href location. By returning false , the engage function does not allow the default action to occur, and the hyperlink navigation will not take place. If the return value were true , the default action would occur after the event handling function's code had executed.

When an event occurs, detailed information about the how, why, and where of that event is written to an event object. In Internet Explorer, this takes the form of a global window.event object, but in other browsers the object is passed as an argument to the event-handling function. This difference is fairly easy to address within the handling function:

Example 13.2. handle_events2.js (excerpt)       
     
function engage(event)      
{      
 if (typeof event == "undefined")      
 {      
   event = window.event;      
 }      
     
 alert("The screen co-ordinates of your click were: " +      
     event.screenX + ", " + event.screenY);      
     
 return false;      
}

The event object allows you to find out a range of details, such as which element was clicked, whether any keys were pressed, the coordinates of the event (eg, where the cursor was located when the mouse button was clicked), and the type of event that triggered the function. Quite a few of the event property names are consistent across browsers, but a few differ. The Mozilla event properties can be viewed at the Gecko DOM Reference , while the Internet Explorer event properties can be seen at MSDN . For properties whose names vary between browsers, the potential for associated problems can normally be rectified with a little object detection; we'll discuss this in detail later in this chapter.

The W3C Way (Event Listeners)

Although the DOM 0 event handlers are quick and easy, they do have limitations (aside from the fact that eventually they will become deprecated). The main advantage of the W3C event listeners is that they natively support the addition and removal of multiple handling functions for the same event on a single element. Event listeners also have the capability to respond to events in several phases (though most browsers don't yet support this capability).

In the W3C specification, an event can be added to an element using the element's addEventListener method, but Internet Explorer for Windows chooses to use a method called attachEvent, which has a slightly different syntax. (Internet Explorer for Mac doesn't support either of these event models, so we have to rely on the DOM 0 handlers to work with events in this browser.)

To add an event listener in every browser except Internet Explorer, you would write code similar to this:

var mylink = document.getElementById("mylink");       
     
mylink.addEventListener("click", engage, false);

To support Internet Explorer, you'd need this code:

var mylink = document.getElementById("mylink");       
     
mylink.attachEvent("onclick", engage);

As well as the differing function names, it's important to note that Internet Explorer uses the DOM 0 handler name for the event -- "onclick" -- rather than the true event name: "click" . The extra argument that's supplied to addEventListener specifies whether the listener is applied during the capture ( true ) or bubble ( false ) event propagation phase. Event propagation is explained in more detail in the discussion below, but bubble is really the most useful choice, and ensures the same behavior in standards-compliant browsers as in Internet Explorer.

The differences between these two approaches are fairly easy to work around using an abstracting function. We can also provide a fallback for browsers that don't support W3C event listeners at the same time:

Example 13.3. handle_events3.js (excerpt)       
     
function attachEventListener(target, eventType, functionRef,      
   capture)      
{      
 if (typeof target.addEventListener != "undefined")      
 {      
   target.addEventListener(eventType, functionRef, capture);      
 }      
 else if (typeof target.attachEvent != "undefined")      
 {      
   target.attachEvent("on" + eventType, functionRef);      
 }      
 else      
 {      
   eventType = "on" + eventType;      
     
   if (typeof target[eventType] == "function")      
   {      
     var oldListener = target[eventType];      
     
     target[eventType] = function()      
     {      
       oldListener();      
     
       return functionRef();      
     };      
   }      
   else      
   {      
     target[eventType] = functionRef;      
   }      
 }      
}

The first two if statements deal with the standards-based and Internet Explorer methods respectively, but the catch-all else deals with older browsers that don't support either of these methods, particularly Internet Explorer 5 for Mac. In this last case, a DOM 0 event handler is used, but to ensure that multiple functions can be used to handle a single event for a particular element, a closure is used to execute any existing functions that are attached to the event.

Closures are an advanced feature of JavaScript that relates to scoping (which you can read about in Chapter 19, Object Orientation in JavaScript). Closures allow an inner function to reference the variables of the containing function even after the containing function has finished running. Simon Willison has explained their usage in relation to event handlers in some detail. Suffice it to say that closures allow us to stack multiple event handlers in browsers that don't support W3C event listeners.

The cross-browser code for assigning an event listener is as follows:

Example 13.4. handle_events3.js (excerpt)       
     
var mylink = document.getElementById("mylink");      
     
attachEventListener(mylink, "click", engage, false);

Not (quite) the Genuine Article

Although the DOM 0 event handler fallback mimics the ability to add multiple event listeners for one event type on an element, it does not provide exact replication of the W3C event model, because specific handlers cannot be removed from an element.

Whereas DOM 0 handlers allowed the cancellation of an element's default action by returning false , W3C event listeners achieve this goal slightly differently. To cancel a default action in this model, we need to modify the event object. Internet Explorer requires you to set its returnValue property to false ; standards-based implementations offer the preventDefault method to do the same thing. We can create a small function that figures out the difference for us:

Example 13.5. handle_events4.js (excerpt)       
     
function stopDefaultAction(event)      
{      
 event.returnValue = false;      
     
 if (typeof event.preventDefault != "undefined")      
 {      
   event.preventDefault();      
 }      
}

We can call this function whenever we want to cancel the default action:

Example 13.6. handle_events4.js (excerpt)       
     
function engage(event)      
{      
 if (typeof event == "undefined")      
 {      
   event = window.event;      
 }      
     
 alert("Engage!");      
     
 stopDefaultAction(event);      
     
 return false;      
}

You still need to return false after executing stopDefaultAction in order to ensure that browsers that don't support the W3C event model will also prevent the default action.

Safari and W3C Event Listeners

Due to a bug in Safari, it's impossible to cancel the default action of clicking a hyperlink in that browser when using W3C event listeners. To achieve the cancellation, you'll have to use DOM 0 event handlers with a return value of false .

Checking for attachEvent

Internet Explorer for Windows actually passes an event object to the event-handling function when attachEvent is used to attach an event listener. However, we still need to check for the existence of this object for any browsers that use the old event model.

One of the advantages of using W3C event listeners is that you can remove an individual listener from an element without disturbing any other listeners on the same event. This is not possible using the DOM 0 handlers.

Internet Explorer uses the detachEvent method, while the standards-compliant browsers instead specify a method called removeEventListener . Each of these methods operates fairly similarly to its listener-adding counterpart: an event type must be supplied along with the function that was assigned to handle that event type. The standard method also demands to know whether the event handler was registered to respond during the capture or bubble phase.

Here's a function that supports this approach across browsers:

Example 13.7. handle_events5.js (excerpt)       
     
function detachEventListener(target, eventType, functionRef,      
   capture)      
{      
 if (typeof target.removeEventListener != "undefined")      
 {      
   target.removeEventListener(eventType, functionRef, capture);      
 }      
 else if (typeof target.detachEvent != "undefined")      
 {      
   target.detachEvent("on" + eventType, functionRef);      
 }      
 else      
 {      
   target["on" + eventType] = null;      
 }      
}

The W3C Event Model and Anonymous Functions

The W3C event model doesn't allow for the removal of anonymous functions, so if you need to remove an event listener, hang onto a reference to the function in question.

In browsers that don't support W3C event listeners, this function removes all event handlers on the given event: it's not possible to remove just one of them and leave the others.

Discussion

Referencing the Target Element

Quite often, you'll want to use the object that was the target of an event inside the event handler itself. With DOM 0 event handlers, the use of the special variable this inside a handling function will refer to the event target object. 考虑以下代码:

Example 13.8. handle_events6.js (excerpt)       
     
var mylink = document.getElementById("mylink");      
     
mylink.onclick = engage;      
...      
function engage()      
{      
 var href = this.getAttribute("href");      
     
 alert("Engage: " + href);      
     
 return false;      
}

Here, this refers to the link with ID mylink . We can use it to get the link's href attribute.

However, if you use W3C event listeners, the target of the event is stored as part of the event object, under different properties in different browsers. Internet Explorer stores the target as srcElement , while the standards model stores it as target . But the element to which these properties point isn't necessarily the element to which the event listener was assigned. It is, in fact, the deepest element in the hierarchy affected by the event. Take a look at the following HTML.

Example 13.9. handle_events6.html (excerpt)       
     
<p>      
 These are the voyages of the <a id="mylink"      
     href="enterprise.html">starship Enterprise</a>.      
</p>

If a click event listener were placed on the paragraph and a user clicked on the link, the paragraph's click event handler would be executed, but the event target that was accessible through the above-mentioned properties would be the hyperlink. Some browsers (most notably, Safari) even go so far as to count the text node inside the link as the target node.

We can write a function that returns the event target irrespective of which property has been implemented, but this does not solve the problem of finding the element to which we originally applied the event listener. (The W3C Standard specifies another property called currentTarget , which lets you get the element to which the listener was assigned, but there is no Internet Explorer equivalent. Browsers that support currentTarget also set up the event handler-style this variable with the same value, but again, without Internet Explorer support, this isn't particularly useful.) Often, the best resolution to this quandary is to iterate upwards from the event target provided by the browser until we find an element that's likely to be the element to which we attached an event listener. To do this, we can perform checks against the element's tag name, class, and other attributes.

The abstracting event target function would look like this:

Example 13.10. handle_events7.js (excerpt)       
     
function getEventTarget(event)      
{      
 var targetElement = null;      
     
 if (typeof event.target != "undefined")      
 {      
   targetElement = event.target;      
 }      
 else      
 {      
   targetElement = event.srcElement;      
 }      
     
 while (targetElement.nodeType == 3 &&      
     targetElement.parentNode != null)      
 {      
   targetElement = targetElement.parentNode;      
 }      
     
 return targetElement;      
}

The if-else retrieves the event target across browsers; the while loop then finds the first non-text-node parent if the target reported by the browser happens to be a text node.

If we want to retrieve the element that was clicked upon, we then make a call to getEventTarget :

Example 13.11. handle_events7.js (excerpt)       
     
var mylink = document.getElementById("mylink");      
     
attachEventListener(mylink, "click", engage, false);      
...      
function engage(event)      
{      
 if (typeof event == "undefined")      
 {      
   event = window.event;      
 }      
     
 var target = getEventTarget(event);      
     
 while(target.nodeName.toLowerCase() != "a")      
 {      
   target = target.parentNode;      
 }      
     
 var href = target.getAttribute("href");      
     
 alert("Engage: " + href);      
     
 return true;      
}

Because we know, in this case, that the event-handling function will be attached only to links ( <a> tags), we can iterate upwards from the event target, checking for a node name of "a" . The first one we find will be the link to which the handler was assigned; this ensures that we aren't working with some element inside the link (such as a strong or a span ).

Obviously, this method of target finding is not ideal, and cannot be 100% accurate unless you have knowledge of the exact HTML you'll be working with. Recently, much effort has gone into resolving this problem, and quite a few of the proposed solutions offer the same this variable as is available under DOM 0 event handlers, and in browsers that support the W3C Standard for event listeners (not Internet Explorer).

One such solution is to make the event listening function a method of the target object in Internet Explorer. Then, when the method is called, this will naturally point to the object for which the method was called. This requires both the attachEventListener and detachEventListener to be modified:

Example 13.12. handle_events8.js (excerpt)       
     
function attachEventListener(target, eventType, functionRef,      
   capture)      
{      
 if (typeof target.addEventListener != "undefined")      
 {      
   target.addEventListener(eventType, functionRef, capture);      
 }      
 else if (typeof target.attachEvent != "undefined")      
 {      
   var functionString = eventType + functionRef;      
   target["e" + functionString] = functionRef;      
     
   target[functionString] = function(event)      
   {      
     if (typeof event == "undefined")      
     {      
       event = window.event;      
     }      
     target["e" + functionString](event);      
   };      
     
   target.attachEvent("on" + eventType, target[functionString]);      
 }      
 else      
 {      
   eventType = "on" + eventType;      
     
   if (typeof target[eventType] == "function")      
   {      
     var oldListener = target[eventType];      
     
     target[eventType] = function()      
     {      
       oldListener();      
     
       return functionRef();      
     }      
   }      
   else      
   {      
     target[eventType] = functionRef;      
   }      
 }      
}      
     
function detachEventListener(target, eventType, functionRef,      
   capture)      
{      
 if (typeof target.removeEventListener != "undefined")      
 {      
   target.removeEventListener(eventType, functionRef, capture)      
 }      
 else if (typeof target.detachEvent != "undefined")      
 {      
   var functionString = eventType + functionRef;      
     
   target.detachEvent("on" + eventType, target[functionString]);      
     
   target["e" + functionString] = null;      
   target[functionString] = null;      
 }      
 else      
 {      
   target["on" + eventType] = null;      
 }      
}

This line of thinking was well represented in entries to Peter Paul Koch's improved addEvent competition .

Another solution by Dean Edwards totally eschews the W3C event model in favor of implementing DOM 0 event handlers with independent add and remove abilities .

Although both of these solutions may prove to be well written and robust, they're largely untested as of this writing, so we'll stick with the approach whose flaws we know and can handle: the one presented in the main solution. Besides, in practice, the process of iterating to find an event's target isn't as unreliable as it may appear to be.

What is Event Bubbling, and How do I Control it?

You may have noticed that we needed to supply a third argument to the W3C Standard addEventListener method, and that a capture argument was included in our attachEventListener function to cater for this. This argument determines the phase of the event cycle in which the listener operates.

Suppose you have two elements, one nested inside the other:

<p>       
 <a href="untimely_death.html">Nameless Ensign</a>      
</p>

When a user clicks on the link, click events will be registered on both the paragraph and the hyperlink. The question is, which one receives the event first?

The event cycle contains two phases, and each answers this question in a different way. In the capture phase, events work from the outside in, so the paragraph would receive the click first, then the hyperlink. In the bubble phase, events work from the inside out, so the anchor would receive the click before the paragraph.

Internet Explorer and Opera only support bubbling, which is why attachEvent doesn't require a third argument. For browsers that support addEventListener , if the third argument is true, the event will be caught during the capture phase; if it is false, the event will be caught during the bubble phase.

In browsers that support both phases, the capture phase occurs first and is always followed by the bubble phase. It's possible for an event to be handled on the same element in both the capture and bubbling phases, provided you set up listeners for each phase.

These phases also highlight the fact that nested elements are affected by the same event. If you no longer want an event to continue propagating up or down the hierarchy (depending upon the phase) after an event listener has been triggered, you can stop it. In Internet Explorer, this involves setting the cancelBubble property of the event object to true ; in the W3C model, you must instead call its stopPropagation method:

Example 13.13. handle_events9.js (excerpt)       
     
function stopEvent(event)      
{      
 if (typeof event.stopPropagation != "undefined")      
 {      
   event.stopPropagation();      
 }      
 else      
 {      
   event.cancelBubble = true;      
 }      
}

If we didn't want an event to propagate further than our event handler, we'd use this code:

Example 13.14. handle_events9.js (excerpt)       
     
var mylink = document.getElementById("mylink");      
     
attachEventListener(mylink, "click", engage, false);      
     
var paragraph = document.getElementsByTagName("p")[0];      
     
attachEventListener(paragraph, "click", engage, false);      
     
function engage(event)      
{      
 if (typeof event == "undefined")      
 {      
   event = window.event;      
 }      
     
 alert("She canna take no more cap'n!");      
     
 stopEvent(event);      
     
 return true;      
}

Although we have assigned the engage function to listen for the click event on both the link and the paragraph that contains it, the function will only be called once per click, as the event's propagation is stopped by the listener the first time it is called.

Finding the Size of an Element

There are so many variables that affect the size of an element -- content length, CSS rules, font family, font size, line height, text zooming ... the list goes on. Add to this the fact that browsers interpret CSS dimensions and font sizes inconsistently, and you can never predict the dimensions at which an element will be rendered. The only consistent way to determine an element's size is to measure it once it's been rendered by the browser.

Solution

You can tell straight away that it's going to be useful to know exactly how big an element is. Well, the W3C can't help: there's no standardized way to determine the size of an element. Thankfully, the browser-makers have more or less settled on some DOM properties that let us figure it out.

Although box model differences mean that Internet Explorer includes padding and borders inconsistently as part of an element's CSS dimensions, the offsetWidth and offsetHeight properties will consistently return an element's width -- including padding and borders -- across all browsers.

Let's imagine that an element's dimensions were specified in CSS like this:

Example 13.15. find_size_element.css       
     
#enterprise      
{      
 width: 350px;      
 height: 150px;      
 margin: 25px;      
 border: 25px solid #000000;      
 padding: 25px;      
}

We can determine that element's exact pixel width in JavaScript by checking the corresponding offsetWidth and offsetHeight properties:

Example 13.16. find_size_element.js (excerpt)       
     
var starShip = document.getElementById("enterprise");      
var pixelWidth = starShip.offsetWidth;      
var pixelHeight = starShip.offsetHeight;

In Internet Explorer 6, Opera, Mozilla, and Safari, the variable pixelWidth will now be set to 450, and the variable pixelHeight will be set to 250. In Internet Explorer 5/5.5, pixelWidth will be 350 and pixelHeight 150, because those are the dimensions at which the broken box model approach used in those browsers will render the element. The values are different across browsers, but only because the actual rendered size differs as well. The offset dimensions consistently calculate the exact pixel dimensions of the element.

If we did not specify the dimensions of the element, and instead left its display up to the default block rendering (thus avoiding the box model bugs), the values would be comparable between browsers (allowing for scrollbar width differences, fonts, etc.).

Attaining the Correct Dimensions

In order to correctly determine the dimensions of an element you must wait until the browser has finished rendering that element, otherwise the dimensions may be different from those the user ends up seeing. There's no guaranteed way to ensure that a browser has finished rendering an element, but it's normally safe to assume that once a window's load event has fired, all elements have been rendered.

Discussion

It is possible to retrieve the dimensions of an element minus its borders, but including its padding. These values are accessed using the clientWidth and clientHeight properties, and for the example element used above their values would be 300 and 100 in Internet Explorer 5/5.5, and 400 and 200 in all other browsers.

There is no property that will allow you to retrieve an element's width without borders or padding.

Finding the Position of an Element

Knowing the exact position of an element is very helpful when you wish to position other elements relative to it. However, because of different browser sizes, font sizes, and content lengths, it's often impossible to hard-code the position of an element before you load a page. JavaScript offers a method to ascertain any element's position after the page has been rendered, so you can know exactly where your elements are located.

Solution

The offsetTop and offsetLeft properties tell you the distance between the top of an element and the top of its offsetParent . But what is offsetParent ? Well, it varies widely for different elements and different browsers. Sometimes it's the immediate containing element; other times it's the html element; at other times it's nonexistent.

Thankfully, the solution is to follow the trail of offsetParents and add up their offset positions -- a method that will give you the element's accurate absolute position on the page in every browser.

If the element in question has no offsetParent , then the offset position of the element itself is enough; otherwise, we add the offsets of the element to those of its offsetParent, then repeat the process for its offsetParent (if any):

Example 13.17. find_position_of_element.js (excerpt)        
       
function getPosition(theElement)        
{        
 var positionX = 0;        
 var positionY = 0;        
       
 while (theElement != null)        
 {        
   positionX += theElement.offsetLeft;        
   positionY += theElement.offsetTop;        
   theElement = theElement.offsetParent;        
 }        
       
 return [positionX, positionY];        
}

IE 5 for Mac Bug

Internet Explorer 5 for Mac doesn't take the body's margin or padding into account when calculating the offset dimensions, so if you desire accurate measurements in this browser, you should have zero margins and padding on the body.

Discussion

The method above works for simple and complex layouts; however, you may run into problems when one or more of an element's ancestors has its CSS position property set to something other than static (the default).

There are so many possible combinations of nested positioning and browser differences that it's almost impossible to write a script that takes them all into account. If you are working with an interface that uses a lot of relative or absolute positioning, it's probably easiest to experiment with specific cases and write special functions to deal with them. Here are just a few of the differences that you might encounter:

  • In Internet Explorer for Windows and Mozilla/Firefox, any element whose parent is relatively positioned will not include the parent's border in its own offset; however, the parent's offset will only measure to the edge of its border. Therefore, the sum of these values will not include the border distance.
  • In Opera and Safari, any absolutely or relatively positioned element whose offsetParent is the body will include the body's margin in its own offset. The body's offset will include its own margin as well.
  • In Internet Explorer for Windows, any absolutely positioned element inside a relatively positioned element will include the relatively positioned element's margin in its offset. The relatively positioned element will include its margin as well.
Detecting the Position of the Mouse Cursor

When working with mouse events, such as mouseover or mousemove, you will often want to use the coordinates of the mouse cursor as part of your operation (eg, to position an element near the mouse). The solution explained below is actually a more reliable method of location detection than the element position detection method we discussed in the section called "Finding the Position of an Element", so if it's possible to use the following solution instead of the previous one, go for it!

Solution

The event object contains everything you need to know to work with the position of the cursor, although a little bit of object detection is required to ensure you get equivalent values across all browsers.

The standard method of obtaining the cursor's position relative to the entire page is via the pageX and pageY properties of the event object. Internet Explorer doesn't support these properties, but it does include some properties that are almost the ones we want. clientX and clientY are available in Internet Explorer, though they measure the distance from the mouse cursor to the edges of the browser window. In order to find the position of the cursor relative to the entire page, we need to add the current scroll position to these dimensions. This technique was covered in Chapter 7, Working with Windows and Frames; let's use the getScrollingPosition function from that solution to retrieve the required dimensions:

Example 13.18. detect_mouse_cursor.js (excerpt)        
       
function displayCursorPosition(event)        
{        
 if (typeof event == "undefined")        
 {        
   event = window.event;        
 }        
       
 var scrollingPosition = getScrollingPosition();        
 var cursorPosition = [0, 0];        
       
 if (typeof event.pageX != "undefined" &&        
     typeof event.x != "undefined")        
 {        
   cursorPosition[0] = event.pageX;        
   cursorPosition[1] = event.pageY;        
 }        
 else        
 {        
   cursorPosition[0] = event.clientX + scrollingPosition[0];        
   cursorPosition[1] = event.clientY + scrollingPosition[1];        
 }        
       
 var paragraph = document.getElementsByTagName("p")[0];        
       
 paragraph.replaceChild(document.createTextNode(        
     "Your mouse is currently located at: " + cursorPosition[0] +        
     "," + cursorPosition[1]), paragraph.firstChild);        
       
 return true;        
}
 clientX / clientY are valid W3C DOM event properties that exist in most browsers, so we can't rely on their existence as an indication that we need to use them. Instead, within our event handler, we test for the existence of pageX . Internet Explorer for Mac does have pageX , but it's an incorrect value, so we must also check for x . x is actually a nonstandard property, but most browsers support it (the exceptions being Opera 8+ and Internet Explorer). It's okay that Opera 8+ doesn't support x , because the else statement is actually a cross-browser method for calculating the mouse cursor position except in Safari, which incorrectly gives clientX the same value as pageX . That's why we still need to use both methods of calculating the cursor position.

Displaying a Tooltip when you Mouse Over an Element

Tooltips are a helpful feature in most browsers, but they can be a bit restrictive if you plan to use them as parts of your interface. If you'd like to use layers that appear when you want them to, aren't truncated, and can contain more than plain text, why not make your own enhanced tooltips?

Solution

For this example, we'll apply a class , hastooltip , on all the elements for which we'd like tooltips to appear. We'll get the information that's going to appear in the tooltip from each element's title attribute:

Example 13.19. tooltips.html (excerpt)        
       
<p>        
 These are the voyages of the <a class="hastooltip"        
     href="enterprise.html" title="USS Enterprise (NCC-1701) ...">        
   starship Enterprise</a>.        
</p>

From our exploration of browser events earlier in this chapter, you'll probably already have realized that we need to set up some event listeners to let us know when the layer should appear and disappear.

Tooltips classically appear in a fixed location when you mouse over an element, and disappear when you mouse out. Some implementations of JavaScript tooltips also move the tooltip as the mouse moves over the element, but I personally find this annoying. In this solution, we'll focus on the mouseover and mouseout events:

Example 13.20. tooltips.js (excerpt)        
       
addLoadListener(initTooltips);        
       
function initTooltips()        
{        
 var tips = getElementsByAttribute("class", "hastooltip");        
       
 for (var i = 0; i < tips.length; i++)        
 {        
   attachEventListener(tips[i], "mouseover", showTip, false);        
   attachEventListener(tips[i], "mouseout", hideTip, false);        
 }        
       
 return true;        
}

We've already coded quite a few of the functions in this script, including addLoadListener from Chapter 1, Getting Started with JavaScript, getElementsByAttribute from Chapter 5, Navigating the Document Object Model, and the attachEventListener function that we created earlier in this chapter, so the bulk of the code is in the event listener functions:

Example 13.21. tooltips.js (excerpt)        
       
function showTip(event)        
{        
 if (typeof event == "undefined")        
 {        
   event = window.event;        
 }        
       
 var target = getEventTarget(event);        
       
 while (target.className == null ||        
     !/(^| )hastooltip( |$)/.test(target.className))        
 {        
   target = target.parentNode;        
 }        
       
 var tip = document.createElement("div");        
 var content = target.getAttribute("title");        
       
 target.tooltip = tip;        
 target.setAttribute("title", "");        
       
 if (target.getAttribute("id") != "")        
 {        
   tip.setAttribute("id", target.getAttribute("id") + "tooltip");        
 }        
       
 tip.className = "tooltip";        
 tip.appendChild(document.createTextNode(content));        
       
 var scrollingPosition = getScrollingPosition();        
 var cursorPosition = [0, 0];        
       
 if (typeof event.pageX != "undefined" &&        
     typeof event.x != "undefined")        
 {        
   cursorPosition[0] = event.pageX;        
   cursorPosition[1] = event.pageY;        
 }        
 else        
 {        
   cursorPosition[0] = event.clientX + scrollingPosition[0];        
   cursorPosition[1] = event.clientY + scrollingPosition[1];        
 }        
       
 tip.style.position = "absolute";        
 tip.style.left = cursorPosition[0] + 10 + "px";        
 tip.style.top = cursorPosition[1] + 10 + "px";        
 document.getElementsByTagName("body")[0].appendChild(tip);        
       
 return true;        
}

After getting a cross-browser event object, and iterating from the base event target element to one with a class of hastooltip , showtip goes about creating the tooltip (a div ). The content for the tooltip is taken from the title attribute of the target element, and placed into a text node inside the tooltip.

To ensure that the browser doesn't display a tooltip of its own on top of our enhanced tooltip, the title of the target element is then cleared -- now, there's nothing for the browser to display as a tooltip, so it can't interfere with the one we've just created. Don't worry about the potential accessibility issues caused by removing the title : we'll put it back later.

Controlling Tooltip Display in Opera

Opera still displays the original title even after we set it to an empty string. If you wish to avoid tooltips appearing in this browser, you'll have to stop the default action of the mouseover using the stopDefaultAction function from the section called "Handling Events", the first section of this chapter. Be aware that this will also affect other mouseover behavior, such as the status bar address display for hyperlinks.

To provide hooks for the styling of our tooltip, we assign the tooltip element an ID that's based on the target element's ID ( targetIDtooltip ), and set a class of tooltip . Although this approach allows for styles to be applied through CSS, we are unable to calculate the tooltip's position ahead of time, so we must use the coordinates of the mouse cursor, as calculated when the event is triggered, to position the tooltip (with a few extra pixels to give it some space).

All that remains is to append the tooltip element to the body, so it will magically appear when we mouse over the link! With a little bit of CSS, it could look like Figure 13.1, "A dynamically generated layer that appears on mouseover".

A dynamically generated layer that appears on mouseover
Figure 13.1. A dynamically generated layer that appears on mouseover

When the mouse is moved off the element, we delete the tooltip from the document, and it will disappear:

Example 13.22. tooltips.js (excerpt)        
       
function hideTip(event)        
{        
 if (typeof event == "undefined")        
 {        
   event = window.event;        
 }        
       
 var target = getEventTarget(event);        
       
 while (target.className == null ||        
     !/(^| )hastooltip( |$)/.test(target.className))        
 {        
   target = target.parentNode;        
 }        
       
 if (target.tooltip != null)        
 {        
   target.setAttribute("title",        
       target.tooltip.childNodes[0].nodeValue);        
   target.tooltip.parentNode.removeChild(target.tooltip);        
 }        
       
 return false;        
}

Earlier, in showTip , we created a reference to the tooltip element as a property of the target element. Having done that, we can remove it here without needing to search through the entire DOM. Before we remove the tooltip, we retrieve its content and insert it into the title of the target element, so we can use it again later.

Do those Objects Exist?

You should check that objects created in other event listeners actually exist before attempting to manipulate them, because events can often misfire, and you can't guarantee that they will occur in a set order.

Discussion

One problem with the code above is that if the target element is close to the right or bottom edge of the browser window, the tooltip will be cut off. To avoid this, we need to make sure there's enough space for the tooltip, and position it accordingly.

By checking, in each dimension, whether the mouse position is less than the browser window size minus the tooltip size, we can tell how far to move the layer in order to get it onto the screen:

Example 13.23. tooltips2.js (excerpt)        
       
function showTip(event)        
{        
 if (typeof event == "undefined")        
 {        
   event = window.event;        
 }        
       
 var target = getEventTarget(event);        
       
 while (target.className == null ||        
     !/(^| )hastooltip( |$)/.test(target.className))        
 {        
   target = target.parentNode;        
 }        
       
 var tip = document.createElement("div");        
 var content = target.getAttribute("title");        
       
 target.tooltip = tip;        
 target.setAttribute("title", "");        
       
 if (target.getAttribute("id") != "")        
 {        
   tip.setAttribute("id", target.getAttribute("id") + "tooltip");        
 }        
       
 tip.className = "tooltip";        
 tip.appendChild(document.createTextNode(content));        
       
 var scrollingPosition = getScrollingPosition();        
 var cursorPosition = [0, 0];        
       
 if (typeof event.pageX != "undefined" &&        
     typeof event.x != "undefined")        
 {        
   cursorPosition[0] = event.pageX;        
   cursorPosition[1] = event.pageY;        
 }        
 else        
 {        
   cursorPosition[0] = event.clientX + scrollingPosition[0];        
   cursorPosition[1] = event.clientY + scrollingPosition[1];        
 }        
       
 tip.style.position = "absolute";        
 tip.style.left = cursorPosition[0] + 10 + "px";        
 tip.style.top = cursorPosition[1] + 10 + "px";        
 tip.style.visibility = "hidden";        
       
 document.getElementsByTagName("body")[0].appendChild(tip);        
       
 var viewportSize = getViewportSize();        
       
 if (cursorPosition[0] - scrollingPosition[0] + 10 +        
     tip.offsetWidth > viewportSize[0] - 25)        
 {        
   tip.style.left = scrollingPosition[0] + viewportSize[0] - 25 -        
       tip.offsetWidth + "px";        
 }        
 else        
 {        
   tip.style.left = cursorPosition[0] + 10 + "px";        
 }        
       
 if (cursorPosition[1] - scrollingPosition[1] + 10 +        
     tip.offsetHeight > viewportSize[1] - 25)        
 {        
   if (event.clientX > (viewportSize[0] - 25 - tip.offsetWidth))        
   {        
     tip.style.top = cursorPosition[1] - tip.offsetHeight - 10 +        
         "px";        
   }        
   else        
   {        
     tip.style.top = scrollingPosition[1] + viewportSize[1] -        
         25 - tip.offsetHeight + "px";        
   }        
 }        
 else        
 {        
   tip.style.top = cursorPosition[1] + 10 + "px";        
 }        
       
 tip.style.visibility = "visible";        
       
 return true;        
}

This function is identical to the previous version until we get to the insertion of the tooltip element. Just prior to inserting the element, we set its visibility to "hidden" . This means that when it's placed on the page, the layer will occupy the same space it would take up if it were visible, but the user won't see it on the page. This allows us to measure the tooltip's dimensions, then reposition it without the user seeing it flash up in its original position.

In order to detect whether the layer displays outside of the viewport, we use the position of the cursor relative to the viewport. This could theoretically be obtained by using clientX/clientY, but remember: Safari gives an incorrect value for this property. Instead, we use our cross-browser values inside cursorPosition and subtract the scrolling position (which is the equivalent of clientX/clientY). The size of the viewport is obtained using the getViewportSize function we created in Chapter 7, Working with Windows and Frames, then, for each dimension, we check whether the cursor position plus the size of the layer is greater than the viewport size (minus an allowance for scrollbars).

If part of the layer is going to appear outside the viewport, we position it by subtracting its dimensions from the viewport size; otherwise, it's positioned normally, using the cursor position.

The only other exception to note is that if the layer would normally appear outside the viewport in both dimensions, when we are positioning it vertically, it is automatically positioned above the cursor. This prevents the layer from appearing directly on top of the cursor and triggering a mouseout event. It also prevents the target element from being totally obscured by the tooltip, which would prevent the user from clicking on it.

Measuring Visible Tooltip Dimensions

In order for the dimensions of the tooltip to be measured it must first be appended to the document. This will automatically make it appear on the page, so to prevent the user seeing it display in the wrong position, we need to hide it. We do so by setting its visibility to "hidden" until we have finalized the tooltip's position.

We can't use the more familiar display property here, because objects with display set to "none" are not rendered at all, so they have no dimensions to measure.

Sorting Tables by Column

Tables can be a mine of information, but only if you can understand them properly. Having the ability to sort a table by its different columns allows users to view the data in a way that makes sense to them, and ultimately provides the opportunity for greater understanding.

Solution

To start off, we'll use a semantically meaningful HTML table. This will provide us with the structure we need to insert event listeners, inject extra elements, and sort our data:

Example 13.24. sort_tables_by_columns.html (excerpt)        
       
<table class="sortableTable" cellspacing="0"        
   summary="Statistics on Star Ships">        
 <thead>        
   <tr>        
     <th class="c1" scope="col">        
       Star Ship Class        
     </th>        
     <th class="c2" scope="col">        
       Power Output (Terawatts)        
     </th>        
     <th class="c3" scope="col">        
       Maximum Warp Speed        
     </th>        
     <th class="c4" scope="col">        
       Captain's Seat Comfort Factor        
     </th>        
   </tr>        
 </thead>        
 <tbody>        
   <tr>        
     <td class="c1">        
       USS Enterprise NCC-1701-A        
     </td>        
     <td class="c2">        
       5000        
     </td>        
     <td class="c3">        
       6.0        
     </td>        
     <td class="c4">        
       4/10        
     </td>        
   </tr>

First, we need to set up event listeners on each of our table heading cells. These will listen for clicks to our columns, and trigger a sort on the column that was clicked:

Example 13.25. sort_tables_by_columns.js (excerpt)        
       
function initSortableTables()        
{        
 if (identifyBrowser() != "ie5mac")        
 {        
   var tables = getElementsByAttribute("class", "sortableTable");        
       
   for (var i = 0; i < tables.length; i++)        
   {        
     var ths = tables[i].getElementsByTagName("th");        
       
     for (var k = 0; k < ths.length; k++)        
     {        
       var newA = document.createElement("a");        
       newA.setAttribute("href", "#");        
       newA.setAttribute("title",        
           "Sort by this column in descending order");        
       
       for (var m = 0; m < ths[k].childNodes.length; m++)        
       {        
         newA.appendChild(ths[k].childNodes[m]);        
       }        
       
       ths[k].appendChild(newA);        
       
       attachEventListener(newA, "click", sortColumn, false);        
     }        
   }        
 }        
       
 return true;        
}

Internet Explorer 5 for Mac has trouble dealing with dynamically generated table content, so we have to specifically exclude it from making any of the tables sortable.

Only tables with the class sortableTable will be turned into sortable tables, so initSortableTable navigates the DOM to find the table heading cells in these tables. Once they're found, the contents of each heading cell are wrapped in a hyperlink -- this allows keyboard users to select a column to sort the table by -- and an event listener is set on these links to monitor click events, and execute sortColumn in response. The title attribute of each link is also set, providing the user with information on what will happen when the link is clicked.

The sortColumn function is fairly lengthy, owing to the fact that it must navigate and rearrange the entire table structure each time a heading cell is clicked:

Example 13.26. sort_tables_by_columns.js (excerpt)        
       
function sortColumn(event)        
{        
 if (typeof event == "undefined")        
 {        
   event = window.event;        
 }        
       
 var targetA = getEventTarget(event);        
       
 while (targetA.nodeName.toLowerCase() != "a")        
 {        
 targetA = targetA.parentNode;        
 }        
       
 var targetTh = targetA.parentNode;        
 var targetTr = targetTh.parentNode;        
 var targetTrChildren = targetTr.getElementsByTagName("th");        
 var targetTable = targetTr.parentNode.parentNode;        
 var targetTbody = targetTable.getElementsByTagName("tbody")[0];        
 var targetTrs = targetTbody.getElementsByTagName("tr");        
 var targetColumn = 0;        
       
 for (var i = 0; i < targetTrChildren.length; i++)        
 {        
   targetTrChildren[i].className = targetTrChildren[i].className.        
       replace(/(^| )sortedDescending( |$)/, "$1");        
   targetTrChildren[i].className = targetTrChildren[i].className.        
       replace(/(^| )sortedAscending( |$)/, "$1");        
       
   if (targetTrChildren[i] == targetTh)        
   {        
     targetColumn = i;        
       
     if (targetTrChildren[i].sortOrder == "descending" &&        
         targetTrChildren[i].clicked)        
     {        
       targetTrChildren[i].sortOrder = "ascending";        
       targetTrChildren[i].className += " sortedAscending";        
       targetA.setAttribute("title",        
           "Sort by this column in descending order");        
     }        
     else        
     {        
       if (targetTrChildren[i].sortOrder == "ascending" &&        
           !targetTrChildren[i].clicked)        
       {        
         targetTrChildren[i].className += " sortedAscending";        
       }        
       
       else        
       {        
         targetTrChildren[i].sortOrder = "descending";        
         targetTrChildren[i].className += " sortedDescending";        
         targetA.setAttribute("title",        
             "Sort by this column in ascending order");        
       }        
     }        
       
     targetTrChildren[i].clicked = true;        
   }        
   else        
   {        
     targetTrChildren[i].clicked = false;        
       
     if (targetTrChildren[i].sortOrder == "ascending")        
     {        
       targetTrChildren[i].firstChild.setAttribute("title",        
           "Sort by this column in ascending order");        
     }        
     else        
     {        
       targetTrChildren[i].firstChild.setAttribute("title",        
           "Sort by this column in descending order");        
     }        
   }        
 }        
       
 var newTbody = targetTbody.cloneNode(false);        
       
 for (var i = 0; i < targetTrs.length; i++)        
 {        
   var newTrs = newTbody.childNodes;        
   var targetValue = getInternalText(        
       targetTrs[i].getElementsByTagName("td")[targetColumn]);        
       
   for (var j = 0; j < newTrs.length; j++)        
   {        
     var newValue = getInternalText(        
         newTrs[j].getElementsByTagName("td")[targetColumn]);        
       
     if (targetValue == parseInt(targetValue, 10) &&        
         newValue == parseInt(newValue, 10))        
     {        
       targetValue = parseInt(targetValue, 10);        
       newValue = parseInt(newValue, 10);        
     }        
     else if (targetValue == parseFloat(targetValue) &&        
         newValue == parseFloat(newValue))        
     {        
       targetValue = parseFloat(targetValue, 10);        
       newValue = parseFloat(newValue, 10);        
     }        
       
     if (targetTrChildren[targetColumn].sortOrder ==        
         "descending")        
     {        
       if (targetValue >= newValue)        
       {        
         break;        
       }        
     }        
     else        
     {        
       if (targetValue <= newValue)        
       {        
         break;        
       }        
     }        
   }        
       
   if (j >= newTrs.length)        
   {        
     newTbody.appendChild(targetTrs[i].cloneNode(true));        
   }        
   else        
   {        
     newTbody.insertBefore(targetTrs[i].cloneNode(true),        
         newTrs[j]);        
   }        
 }        
       
 targetTable.replaceChild(newTbody, targetTbody);        
       
 stopDefaultAction(event);        
       
 return false;        
}

The first for loop that occurs after all the structural variables have been defined sets the respective states for each of the table heading cells when one of them is clicked. Not only are classes maintained to identify the heading cell on which the table is currently sorted, but a special sortOrder property is maintained on each cell to determine the order in which that column is sorted. Initially, a column will be sorted in descending order, but if a heading cell is clicked twice consecutively, the sort order will be changed to reflect an ascending sequence. Each heading cell remembers the sort order state it exhibited most recently, and the column is returned to that state when its heading cell is re-selected. The title of the hyperlink for a clicked heading cell is also rewritten depending upon the current sort order, and what the sort order would be if the user clicked on it again.

The second for loop sorts each of the rows that's contained in the body of the table. A copy of the original tbody is created to store the reordered table rows, and initially this copy is empty. As each row in the original tbody is scanned, the contents of the table cell in the column on which we're sorting is compared with the rows already in the copy.

In order to find the contents of the table cell, we use the function getInternalText :

Example 13.27. sort_tables_by_columns.js (excerpt)        
       
function getInternalText(target)        
{        
 var elementChildren = target.childNodes;        
 var internalText = "";        
       
 for (var i = 0; i < elementChildren.length; i++)        
 {        
   if (elementChildren[i].nodeType == 3)        
   {        
     if (!/^s*$/.test(elementChildren[i].nodeValue))        
     {        
       internalText += elementChildren[i].nodeValue;        
     }        
   }        
   else        
   {        
     internalText += getInternalText(elementChildren[i]);        
   }        
 }        
       
 return internalText;        
}
getInternalText

When sortColumn finds a row in the copy whose sorted table cell value is "less" than the one we're scanning, we insert a copy of the scanned row into the copied tbody. For a column in ascending order, we simply reverse this comparison: the value of the row in the copy must be "greater" than that of the scanned row.

However, before a comparison is made, we check whether the contents of the sorted table cell can be interpreted as an integer or a float; if so, the comparison values are converted. This makes sure that columns that contain numbers are sorted properly; string comparisons will produce different results than number comparisons.

Once all of our original rows have been copied into the new tbody , that element is used to replace the old one, and we have our sorted table!

Using the sortableDescending and sortableAscending classes, which are assigned to the currently sorted table heading cells, we can use CSS to inform the user which column the table is sorted on, and how it is sorted, as shown in Figure 13.2, "A sortable table sorted in descending order on the fourth column" and Figure 13.3, "A sortable table sorted in ascending order on the second column".

A table sorted on its last column in descending order
Figure 13.2. A sortable table sorted in descending order on the fourth column

A table sorted on its second column in ascending order
Figure 13.3. A sortable table sorted in ascending order on the second column

Summary

The two main pillars of DHTML are the capturing of events, and the reorganization and creation of page elements via the DOM. Using these principles, it's possible to capture many of the different ways that users interact with a page and make the interface respond accordingly.

As can be seen by the number and quality of JavaScript-enhanced web applications that are now available, the features DHTML can bring to new interfaces represents one of the biggest growth areas for innovative JavaScript. The foundations and basic examples shown in this chapter give you a sense of the power that it can deliver inside a user's browser. We'll expand upon this further in the following chapters as we build some really interesting interfaces.

That's it for our sample of The JavaScript Anthology: 101 Essential Tips, Tricks & Hacks . What's next?

Download this sample as a PDF , for reading offline. Check out the book's Table of Contents to see what else it covers. And see what others think of the book -- read live customer reviews of The JavaScript Anthology: 101 Essential Tips, Tricks & Hacks .

From: https://www.sitepoint.com/javascript-from-scratch/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值