原文地址:http://www.onlamp.com/pub/a/onlamp/2007/11/20/advanced-javascript-iii.html
JavaScript高级编程I:http://blog.csdn.net/mydeman/archive/2007/08/20/1751479.aspx
JavaScript高级编程II:http://blog.csdn.net/mydeman/archive/2007/08/23/1755760.aspx
在本系列的终结篇中,我们继续学习另外一个有用的JavaScript案例,主要是通过DOM随意操作和重写HTML页面。和以前一样,本文中使用JavaScript应该不经修改就可以在当前所有主要的浏览器上使用。
使用你的鼠标进行魔术变换(Magic Mutating with Your Mouse)
在创建表单时,你有时可能希望根据某些次级条件改变输入域的类型。例如,假设你正在编写一个图书馆的搜索界面。如果通过作者搜索,你可能想使用一个标准文本框。如果通过图书类型(比方说精装本或普通),你可能想提供一个下拉列表;以及如果使用图书内容搜索,就提供一个大文本域。试试下面的,你就会比较清楚了:
Search for: AuthorBindingContent
JavaScript通过一个回调函数实现这个功能,只要改变搜索类型的值,回调函数就会被触发。
// Copies attributes from one node (src) to another (dest). Modify this to suit // your needs. For example, you may not want to retain the value when a new // node is generated. function CopyAttributes(src, dest) { var i; dest.id = src.id; dest.name = src.name; } // Performs the mutation on the node with id given by the argument searchbox. // opt specifies the type of mutation to make. function UpdateSearchField(opt, searchbox) { var inputbox = document.getElementById(searchbox); // Based on search type, choose which element to create and // set its attributes accordingly. if (opt == 'content') { // make a textarea var el = document.createElement("TEXTAREA"); CopyAttributes(inputbox, el); el.cols = 40; el.rows = 4; } else if (opt == 'author') { // make a standard text box var el = document.createElement("INPUT"); CopyAttributes(inputbox, el); el.type = 'text'; el.size = 20; } else if (opt == 'binding') { // make a drop-down list var el = document.createElement("SELECT"); CopyAttributes(inputbox, el); el.size = 1; // str stores the text for the drop-down selector. Different // drop-down lists could be provided by simply switching in // different arrays for str. var i; var str = new Array('Hardcover', 'Paperback', 'Magazine'); for (i = 0; i < str.length; i++) { var opt = document.createElement("OPTION"); opt.appendChild(document.createTextNode(str[i])); opt.setAttribute('value', str[i]); if (inputbox.value == str[i]) // check for selected item opt.setAttribute('selected', 'selected'); el.appendChild(opt); } } // Use the DOM function replaceChild to put in the newly created // node. inputbox.parentNode.replaceChild(el, inputbox); }
表单的布局代码为:
<form onsubmit="return false;" action=""> Search for: <select onchange="UpdateSearchField(this.value, 'searchbox');" name="searchtype"> <option value="author" selected="selected">Author</option> <option value="binding">Binding</option> <option value="content">Content</option> </select> <input id="searchbox" type="text" value="" name="searchbox" size="20" /> <input type="submit" value="Go!" name="submit" /> </form>
这里使用技术其实很简单。无论何时用户改变searchtype的值(通过从下拉列表中选择),onchange属性就会调用我们的JavaScript函数,UpdateSearchField,将刚刚选择的值和表单中搜索框的id作为参数。该函数创建一段HTML代码,然后使用新代码替换当前的搜索框。很显著,我们正在随意重写网页的部分代码。
假设读者已经熟悉HTML DOM。如果不熟悉,可以参考官方文档。UpdateSearchField首先观察选中域的值,然后创建一个新的合适类型的DOM元素节点。通常情况下, 你可能使用节点类型作为输入参数,或者在表中查询,但是为了简单其间,我们基于传递搜索域的值。现在解释一下createElement。该函数在特定的document(document,在这里指当前document)的DOM树中创建一个新的空节点。例如,创建一个INPUT节点等同于HTML的<INPUT></INPUT>。注意当创建时,它还并没有放在文档的任何地方。可以想象为,它被存储屏幕外的任何地方,在浏览器窗口之外,在它被实际添加到当前文档的某个地方之前,允许我们根据需要创建和修改它。
然后,我们复制input(它的id已经作为传入参数--该节点最终会被我们的新节点替代)的部分或者所有属性到新节点中,此时用到了助手函数CopyAttributes。你可能需要改变这个函数以适应你的需求。例如,你可能想要复制对象类或者对象值。复制以后,节点的属性就设置好了。
对于SELECT元素,我们就需要多一点工作了,需要为它创建OPTION。这个在一个循环中完成,和创建其他元素的方式一样,使用createElement和createTextNode。后面的工作和前面一样,只不过不是创建HTML标签,而是创建一个包含纯文本的DOM节点。它是包含在HTML的OPTION标签之间的文本。然后,使用DOM函数appendChild将这些option插入到SELECT标签中间。如果一步步地看for循环中的过程,这个过程可能更加容易理解。
循环次数 | 生成的HTML |
0 | <SELECT> </SELECT> |
1 | <SELECT> <OPTION value="HardCover">HardCover</OPTION> </SELECT> |
2 | <SELECT> <OPTION value="HardCover">HardCover</OPTION> <OPTION value="Paperback">Paperback</OPTION> </SELECT> |
3 | <SELECT> <OPTION value="HardCover">HardCover</OPTION> <OPTION value="Paperback">Paperback</OPTION> <OPTION value="Magazine">Magazine</OPTION> </SELECT> |
最后,我们新的searchbox节点终于创建完毕,我们需要将它放在页面的某个位置。同时需要删除现在的searchbox。很方便地,replaceChild函数就是用来满足我们需要的,使用我们创建的新节点替换已存在的节点(以及它的子节点,如果有的话)。这样,我们创建的新HTML就替换了已有的id为searchbox的HTML代码块。至此,大功告成。
当然,这种技巧并不局限于搜索表单中的INPUT框,实际上通过页面上某些事件的触发,你可以将任何元素变为任何其他的元素。增加一点想象力,许多有趣和令人印象深刻效果就可以通过该技术实现。
动态表格(Dynamic Tables)
在用户界面上经常期望用户可以输入不确定函数的数据。例如,当填写订单时,用户会一行输入一条记录。有了JavaScript,我们再也不用担心是否为订单预留了足够的控件,让用户根据需要添加和删除行就可以了。下面是一个即时的演示,在上个例子的基础上更近一步。
Widgets "R" Us Order Form
|
||||||||||||||
Grand Total: |
要在最后增加一行,只需要Append Row按钮。要删除一行,就点击行尾的红色“X”。当只剩下最后一行时就不能删除了。另外注意,只要你输入了数量和价格或者删除了一行,Total和Grand Total的值就会更新。这些是如何实现的呢?首先,我们使用HTML创建一个初始的表格。注意,初始时必须至少有一行。
<form action="" method="post"> <table> <tr> <td colspan="3"> <table id="catalog" border="1"> <tr> <th>Catalog #</th> <th>Description</th> <th>Quantity</th> <th>Unit Price</th> <th>Total</th> <th> </th> </tr> <tr> <td> <input id="catno_1" name="catno_1" tabindex="1" type="text" value="" /> </td> <td> <input id="descr_1" name="descr_1" type="text" value="" /> </td> <td> <input id="quant_1" name="quant_1" type="text" value="" onkeyup="UpdateTotals('catalog');" /> </td> <td> <input id="price_1" name="price_1" type="text" value="" onkeyup="UpdateTotals('catalog');" /> </td> <td> <input id="total_1" name="total_1" type="text" value="" disabled="disabled" /> </td> <td> <span id="delete_1" style="color:red; cursor: pointer;" onclick="DeleteRow(this);"> </span> </td> </tr> </table> </td> </tr> <tr> <td> <input type="submit" name="submit" value="Append Row" onclick="AppendRow('catalog'); return false;"/> </td> <td align="right"> <b>Grand Total:</b> <input id="total" name="total" type="text" value="" disabled="disabled" /> </td> <td> </td> </tr> </table> </form>
这只是一个由表单包围的标准HTML表格,其每一个单元格都包含一个供用户输入数据的input标签。注意,每一个input标签的name和id值的都是以“_1”结尾,删除按钮的id亦是如此。它用来保证标签的唯一性,同时表明标签属于哪一行——当前就是第一行。例如,第3行中price列的name和id就是price_3,等等。Total和Grand Total域是不可用的,因为它们是计算列,是只读的。所有的任务都有三个JavaScript函数完成:AppendRow与同名按钮相关联,DeleteRow与删除X关联,以及UpdateTotal,只要在Quantity或者Price域按下某个键就会被触发,计算总和。首先来看第一个函数:
// Reads the quantity and price columns in the form and computes the // totals and grand total, filling these in. function UpdateTotals(table_id) { var numrows = document.getElementById(table_id).rows.length - 1; // don't count the header row! var i, totalcost = 0.00; for (i = 1; i <= numrows; i++) { // Compute total for each row var q = parseInt(document.getElementById('quant_' + i).value); var price = parseFloat(document.getElementById('price_' + i).value); var cost; if (!q || !price) cost = 0.00; else cost = q * price; var total = document.getElementById('total_' + i); total.value = '$' + cost; totalcost = totalcost + cost; // Keep running grand total } var total = document.getElementById('total'); total.value = '$' + totalcost; }
这个函数使用表格id作为唯一的输入参数,计算表单的总数和总数之和。它首先计算行数,然后在其上循环。注意,第1行(下标为0)是表格的头,所以我们跳过它从第2行(下标为1)开始。它读取每一行的数量和价格信息,这里就利用了这些列id的命名规则;接着,两者值都不为0,则将其相乘计算总数。计算结果保存到当前行的Total列中。总数之和也被计算出来放在Grand Total域中。注意,尽管它们处于不可用状态,我们仍然可以使用JavaScript函数对其进行赋值。现在,我们来看AppendRow函数:
// Appends a row to the given table, at the bottom of the table. function AppendRow(table_id) { var row = document.getElementById(table_id).rows.item(1); // 1st row var newid = row.parentNode.rows.length; // Since this includes the header row, we don't need to add one var newrow = row.cloneNode(true); rowrenumber(newrow, newid); row.parentNode.appendChild(newrow); // Attach to table // Clear out data from new row. var curnode = document.getElementById('catno_' + newid); curnode.value = ""; curnode.tabIndex = newid; curnode = document.getElementById('descr_' + newid); curnode.value = ""; curnode = document.getElementById('quant_' + newid); curnode.value = ""; curnode = document.getElementById('price_' + newid); curnode.value = ""; curnode = document.getElementById('total_' + newid); curnode.value = ""; curnode = document.getElementById('delete_' + newid); curnode.innerHTML = "X"; curnode = document.getElementById('delete_1'); // Really only need this when newid = 2 curnode.innerHTML = "X"; }
再次,表格的id是唯一的输入参数。我们获取DOM树中表格的第一个tr的句柄放在变量row中,然后检查其父节点以获取表格中总行数。接着,我们使用DOM函数cloneNode创建一个当前行的拷贝。这个函数的输入参数表示我们最好还是包含所有子节点,所以我们获得整个行的拷贝,而不是一个空的tr节点。助手函数,rowrenumber,用来根据相应的行数(它是表单可输入行的总数加1)修改所有id。DOM函数appendChild将新行添加到表格中。最后,我们把输入域,因为它们复制于第一行——我们只想复制结构,而不复制数据。X被放在第1行的删除列中,新行中也是(当只剩下一行时它就被空白替代,这些用户就不会尝试删除了)。我们来简单看一下重新编号id的函数:
// Given a tr node and row number (newid), this iterates over the row in the // DOM tree, changing the id attribute to refer to the new row number. function rowrenumber(newrow, newid) { var curnode = newrow.firstChild; // td node while (curnode) { var curitem = curnode.firstChild; // input node (or whatever) while (curitem) { if (curitem.id) { // replace row number in id var idx = 0; var spl = curitem.id.split('_'); var baseid = spl[0]; curitem.id = baseid + '_' + newid; if (curitem.name) curitem.name = baseid + '_' + newid; if (baseid == 'catno') curitem.tabIndex = newid; } curitem = curitem.nextSibling; } curnode = curnode.nextSibling; } }
这个从以变量newrow传入的tr节点开始遍历DOM树。它完成两个级别的遍历,查找所有没有id的域。我们只做两个级别深入(例如,for循环的两个嵌套),因为我们知道在表格中这些id在什么地方。第一级是td标签,第二级是input标签,也就是我们的所有id所在的地方。因为我们对id采用了特别的命名方{type}_{rownumber},现在我们就可以利用它,使用JavaScript的split函数将字符串分开,然后使用新的行号替换旧的。我们同时更新name域,如果存在的话,以及第一列的tabIndex。
表单的最后一部分是删除行的函数:
// Give a node within a row of the table (one level down from the td node), // this deletes that row, renumbers the other rows accordingly, updates // the Grand Total, and hides the delete button if there is only one row // left. function DeleteRow(el) { var row = el.parentNode.parentNode; // tr node var rownum = row.rowIndex; // row to delete var tbody = row.parentNode; // tbody node var numrows = tbody.rows.length - 1; // don't count header row! if (numrows == 1) // can't delete when only one row left return false; var node = row; tbody.removeChild(node); var newid = -1; // Loop through tr nodes and renumber - only rows numbered // higher than the row we just deleted need renumbering. row = tbody.firstChild; while (row) { if (row.tagName == 'TR') { newid++; if (newid >= rownum) rowrenumber(row, newid); } row = row.nextSibling; } if (numrows == 2) { // 2 rows before deleting - only 1 left now, so 'hide' delete button var delbutton = document.getElementById('delete_1'); delbutton.innerHTML = ' '; } UpdateTotals(tbody.parentNode.id); // Grand Total may change after a delete, so update it }
在HTML中,这个函数在span标签中被调用,以this作为输入参数。因此进入该函数,el就是被点击的span标签的指针,并且我们必须向上走两级找到DOM树中的tr节点。我们也可以使用this.parentNode.parentNode作为输入参数。首先,我们必须保证至少有两行,因为只剩下一行时是不允许删除的。然后,我们使用DOM函数removeChild删除行节点。注意,它是作为父元素tbody节点的方法被调用的。
接下来,我们遍历表格树在每一个tr节点上停止。在被删除行以后的行,如果有的话,都需要重新编号,因为它们行号已经减少了1(例如,删除行4意味着行5变成行4,行6变成行5,等等,而行1、2和3保持不变)。我们使用方便的rowrenumber函数完成这个任务,这个函数在前面已经讨论过。然后,我们停下来检查是不是只剩下一行(也就是说,在删除的行前要有两行)。如果是这样,我们使用空白替换X来隐藏删除按钮,以使用户不会尝试删除它(但是如果用户不知何故地要删除,什么都不会发生,因为在上面我们已经对这种情况做了检查)。最后,删除一行意味着Grand Total的值可能改变,除非删除行为空,所以我们调用UpdateTotals保证计算结果保持一致。
这可能是到现在为止我们看到的最复杂的例子了,但是如果你将它拆分为不同的组成部分,你会发现其实非常简单。这个例子大量利用了DOM函数和DOM树,并且很有希望能让你对按照自己的意愿动态操作HTML页面有一个初步认识。作为练习,尝试为每一行增加一列,该列包含一个Insert Row按钮,点击按钮可以在当前行之前插入一空白行。提示:应该只需对AppendRow函数做一下简单调整就可以实现。
Elusive Text
在这个最后的例子中,仍然是使用DOM,我们将学习一个简单的方法,用来在输入域中显示临时的文本,这个文本在输入域不完全为空时消失。这个方法可以用来节省空间,例如,直接把输入域的标题放在输入框内而不是左边和上面。下面就是一个可以运行的例子:
Enter Login Name
Enter Password
创建这个简单的页面的HTML代码如下所示:
<style type="text/css"> .helptext { position: absolute; color: #999999; z-index: 10; font-size: small; overflow: hidden; } </style> <form action="" method="post"> <input type="text" name="login" value="" onkeyup="UpdateHelpText(this);" onmouseup="UpdateHelpText(this);" /> <div class="helptext" id="label_login" onclick="ChangeFocus(this);"> Enter Login Name </div> <input type="password" name="password" value="" onkeyup="UpdateHelpText(this);" onmouseup="UpdateHelpText(this);" /> <div class="helptext" id="label_password" onclick="ChangeFocus(this);"> Enter Password </div> <input type="submit" name="submit" value="Login" onclick="return false;" /> </form> <script type="text/javascript" language="JavaScript"> UpdateHelpText(document.getElementsByName('login')[0]); UpdateHelpText(document.getElementsByName('password')[0]); </script>
首先我们为浮动文本定义CSS样式。绝对定位用来让文本位于页面上其他元素的最上层(这里是输入框)。同样,z-index设为high用来保证文本出现在最上层,而不会隐藏到其他元素后面。overflow设置为hidden,因为我们不想在文本周围出现滚动条。如果它不能正好符合空白,将会被截断。
这个表单是一个标准的表单,和函数UpdateHelpText相关连,在输入域中有键被按下或释放,或者鼠标被释放时。这个函数,如我们将要看到的,根据输入区域是否为空来隐藏或者显示文本。因此,只要我们开始在输入框中键入信息,浮动文本就会消失。当我们完全删除它的内容是,它又会魔术般重现。我们在表单之后就调用这个函数,初始化文本并把它正确定位在表单内。在每一个input标签之后的是div标签,包含了文本,利用了我们上面创建的helptext CSS类。注意,因为我们设置z-index比较高使文本在最上层,现在就有了一个问题,点击文本时就选中了这个文本而不是移动光标到了输入区域,而后者可能正是用户想要的。为了修正这个问题,在鼠标点击帮助文本时我们调用一个函数ChangeFocus。下面我们看一下这两个JavaScript函数:
// Look for previous INPUT tag and move cursor there. function ChangeFocus(el) { while (el.tagName != 'INPUT') el = el.previousSibling; el.focus(); } // Turn on or off help text depending on content of INPUT field. function UpdateHelpText(el) { var label = el; while (label.tagName != 'DIV') label = label.nextSibling; if (el.value == '') { // Field is empty; show text label.style.left = getElementAbsPosX(el) + 'px'; label.style.top = (getElementAbsPosY(el) - 7) + 'px'; label.style.visibility = 'visible'; } else { // Field is not empty; hide text if (label) label.style.visibility = 'hidden'; } }
ChangeFocus相当简单。它从当前节点开始(div标签),在DOM树中回溯直到找到第一个input标签。在input标签上调用focus将文本光标送到输入域,就好像用户直接点击了input区域。结果就是点击浮动文本就好像直接点击了输入区域。注意,我们可以使用id将div标签和相应的input标签连接起来——例如,input标签的id为login,div的id为login_label,我们就可以很容易从一个节点找到另外一个节点——但是,我们使用HTML页面结构的知识来找到节点。如果我们使用id,那么关于页面结构的知识和假设就没有必要了,因此对于一般的应用程序它可能不失为一种较好的方法。
UpdateHelpText从input标签开始寻找div标签,和上面方法一样。如果input域包含文本,浮动文本使用visibility CSS属性隐藏。否则就是设为了visible显示文本。每次调用这个函数时,文本都被正好放在input域内部。因此,如果你改变浏览器文本大小或者放大倍率,例如,如果文本没有在合适的位置,只需点击input框或者按下一个键就会把文本重新定位到合适的位置。定位函数getElementAbsPosX和getElementAbsPosY获取input元素的以像素为单位的绝对位置,使得浮动文本可以准确定位。关于它们如何工作以及源代码,可以参考本系列的第一篇文章。
总结(Summary)
在这一部分,我们探讨了一些使用JavaScript操作DOM的技巧。通过这些技巧,开发者可以修改HTML页面的任何部分,可以增加原来页面没有的内容,删除内容,甚至可以四处拖拽。这些例子只不过是浅尝辄止,一点想象力就可以实现。有一点很重要,需要指出,尽管现在多数浏览器都允许重写计算机内存中的HTML页面,如果你做了过多的更改,事情可能变得有点使人不太愉快,有些浏览器对这种情况的处理比较好。特别地,像尝试使用回调函数创建新节点的情况在输入变量上可能有些不太容易把握,这种情况在IE和Mozilla家族的浏览器之间的处理方法就大相径庭。我希望你已经发现这几篇文章的意义,也希望他们可以激发您的灵感,使用一小段的高级JavaScript让你网站更加生动有趣。