scratch脚本
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应用程序”中深入研究这些技术。
安全限制
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。 原价$ 11.95 您的完全免费
免费获得这本书
由于JavaScript在高度敏感的数据和程序领域内运行,因此其功能受到限制,以确保不会被恶意使用。 因此,有很多事情是JavaScript根本不允许做的。 例如,它无法从您的计算机读取大多数系统设置,无法直接与硬件进行交互或无法运行程序。
另外,由于该元素的属性,JavaScript中通常不允许特定元素进行某些特定的交互。 例如,更改表单<input>的值 通常是没有问题的,但是如果它是文件输入字段(例如<input type="file"> ),则根本不允许对其进行写操作-一种阻止恶意脚本使用户上传他们没有上传的文件的限制选择。
有很多类似的安全性限制示例,随着本书中介绍的应用程序中出现这些限制,我们将对其进行扩展。 但总而言之,这是JavaScript的主要限制和安全限制的列表,包括我们已经看到的那些限制。 JavaScript无法:
- 直接打开和读取文件(在特定情况下除外,具体情况除外,如第18章,使用JavaScript构建Web应用程序中所述)。
- 在用户计算机上创建或编辑文件(Cookie除外,Cookie除外,在第8章中,使用Cookie进行了讨论)。
- 读取HTTP POST数据。
- 读取系统设置,或从用户计算机读取的其他任何无法通过语言或宿主对象获得的数据(宿主对象是诸如window和screen类的东西,它们是环境提供的,而不是语言本身提供的。)
- 修改文件输入字段的值。
- 更改从其他域加载的文档的显示。
- 关闭或修改脚本未打开的窗口(即主浏览器窗口)的工具栏和其他元素。
最终,可能根本不支持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版本是毫无意义的,因为JavaScript是ECMAScript,并且不推荐使用language属性,而使用type属性。 此属性指定所包含文件的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.addEventListener , document.addEventListener和window.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();};}
但是,请记住该重要词-不必要。 一些空白是必不可少的,例如var和typeof之后的单个空格。
讨论区
除了混淆的好处外,这种做法还有很多优点。 去除注释和不必要的空格的脚本较小; 因此,它们加载速度更快,并且处理速度可能更快。
但是请记住,必须使用分号行终止符和大括号对代码进行严格格式化(正如我们在“使用大括号和分号(一致的编码实践)”一节中所讨论的); 否则,删除换行符会使代码行一起运行,并最终导致错误。
开始压缩之前,请记住要复制脚本。 我知道这似乎很明显,但是我已经多次犯了这个错误,而成为如此基础的人更容易受到伤害! 这些天,我要做的是以完全隔开并带有注释的形式编写和维护脚本,然后在发布之前通过一堆搜索/替换表达式运行它们。 通常,我保留脚本的两个副本,分别名为myscript.js和myscript-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控制台,您可以通过输入以下终端命令来启用该菜单。 ( $表示命令提示符,请勿键入。)
$ 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错误控制台”所示。
图1.1。 Firefox中JavaScript错误控制台
如图1.2“ Opera中JavaScript控制台”所示,Opera提供了一个非常冗长的报告,其中包括对错误产生源的事件的追溯,对发生错误的行的通知以及说明。
当最初由其他代码调用的代码中发生错误时,回溯将提供帮助。 例如,事件处理程序调用一个函数,然后又调用第二个函数,此时就发生了错误。 Opera的控制台将在每个阶段追溯到其发起事件或调用的过程。
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.)
Figure 1.2. The JavaScript console in Opera
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
err
for-in
Example 1.13. debugging-trycatch.js (excerpt)
for (var i in err)
{
alert(i + ': ' + err[i]);
}
innerHTML
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>';
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();
Example 1.16. debugging-writing.js (excerpt)
document.title = '0 = ' + data[0];
- 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.
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.
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.
解
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
getElementsByTagName
getElementsByTagName
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>
Example 5.4. access_element2.js (excerpt)
var anchors = document.getElementsByTagName("a");
getElementsByTagName
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"
className
Example 5.5. access_element2.js (excerpt)
var anchors = document.getElementsByTagName("a");
for (var i = 0; i < anchors.length; i++)
{
anchors[i].className = "starLink";
}
getElementById
getElementsByTagName
getElementsByTagName
getElementsByTagName
getElementsByTagName
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>
ul
getElementsByTagName
Example 5.7. access_element3.js (excerpt)
var starsList = document.getElementById("stars");
var starsAnchors = starsList.getElementsByTagName("a");
starsAnchors
document.forms
document.images
getElementsByTagName
document.body
var body = document.getElementsByTagName("body")[0];
- 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.
解
createElement
Example 5.10. create_elements.js (excerpt)
var newAnchor = document.createElement("a");
newAnchor
createElementNS
createElement
var newAnchor = document.createElementNS(
"http://www.w3.org/1999/xhtml", "a");
removeElement
removeElementNS
getAttribute
getAttributeNS
createTextNode
Example 5.11. create_elements.js (excerpt)
var anchorText = document.createTextNode("monoceros");
nodeValue
var textNode = document.createTextNode("monoceros");
var oldText = textNode.nodeValue;
textNode.nodeValue = "pyxis";
oldText
"monoceros"
textNode
"pyxis"
appendChild
Example 5.12. create_elements.html (excerpt)
<p id="starLinks">
<a href="sirius.html">Sirius</a>
</p>
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);
newChild
<p id="starLinks">
<a href="sirius.htm">Sirius</a><a>monoceros</a>
</p>
insertBefore
replaceChild
Example 5.14. create_elements2.html (excerpt)
<p id="starLinks">
<a id="sirius" href="sirius.html">Sirius</a>
</p>
insertBefore
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);
newChild
<p id="starLinks">
<a>monoceros</a><a id="sirius" href="sirius.htm">Sirius</a>
</p>
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);
<p id="starLinks">
<a>monoceros</a>
</p>
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>
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);
cloneNode
cloneNode
div
div
div
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);
childNodes
id
class
href
Example 5.20. change_type_of_element.js (excerpt)
= paragraph.getAttribute("id");
div.className = paragraph.className;
removeChild
Example 5.21. remove_element.html (excerpt)
<p>
<a id="sirius" href="sirius.html">Sirius</a>
</p>
removeChild
Example 5.22. remove_element.js (excerpt)
var anchor = document.getElementById("sirius");
var parent = anchor.parentNode;
var removedChild = parent.removeChild(anchor);
removedChild
createElement
<p>
</p>
removeChild
var anchor = document.getElementById("sirius");
var parent = anchor.parentNode;
parent.removeChild(anchor);
insertBefore
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>
childNodes
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);
<div id="starContainer">
<a href="aldebaran.htm">Aldebaran</a>
<a href="castor.htm">Castor</a>
<a href="pollux.htm">Pollux</a>
</div>
getAttribute
Example 5.25. read_write_attributes.html (excerpt)
<a id="antares" href="antares.html" title="A far away place">
Antares</a>
Example 5.26. read_write_attributes.js (excerpt)
var anchor = document.getElementById("antares");
var anchorId = anchor.getAttribute("id");
var anchorTitle = anchor.getAttribute("title");
anchorId
"antares"
anchorTitle
"A far away place"
setAttribute
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");
newTitle
"Not that far away"
link.getAttribute("href")
link.href
setAttribute
class
id
style
setAttribute
link.getAttribute("href")
"http://www.example.com/antares.html"
"antares.html"
var anchor = document.getElementById("sirius");
if (anchor.getAttribute("title") &&
anchor.title == "Not the satellite radio")
{
...
}
null
var anchor = document.getElementById("sirius");
anchor.setAttribute("title", "");
anchor.title = "Yes, the satellite radio";
element.style
element.className
class
element.className
getAttribute
setAttribute
element.getAttribute("class")
element.getAttribute("className")
htmlFor
getAttribute
setAttribute
element.getAttribute("for")
element.getAttribute("htmlFor")
type="checkbox"
var inputs = document.getElementsByTagName("input");
for (var i = 0; i < inputs.length; i++)
{
if (inputs.getAttribute("type") == "checkbox")
{
...
}
}
getElementsByAttribute
getElementsByTagName("*")
document.all
getElementsByAttribute
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;
}
getElementsByAttribute
className
className
className
className
className
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;
}
addClass
className
className
b
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;
}
removeClass
className
className
className
- 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.
解
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.
解
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:///" 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.
解
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.
解
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!
解
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!
解
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.
解
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
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 .
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.
解
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.
解
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!
解
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
pageX
pageX
x
x
x
else
clientX
pageX
class
hastooltip
title
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>
mouseover
mouseout
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;
}
addLoadListener
getElementsByAttribute
attachEventListener
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;
}
hastooltip
showtip
div
title
title
title
stopDefaultAction
targetIDtooltip
tooltip
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;
}
showTip
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;
}
"hidden"
cursorPosition
getViewportSize
"hidden"
display
display
"none"
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>
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;
}
sortableTable
initSortableTable
sortColumn
sortColumn
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;
}
for
sortOrder
tbody
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
sortColumn
tbody
sortableDescending
sortableAscending
翻译自: https://www.sitepoint.com/javascript-from-scratch/
scratch脚本