有些事情现在已经很容易被忘记了,但是在开发原创的 Mac OS 的时候,业界到处出现新的图形用户接口(GUI),人们所做的工作差别相当小。Macintosh 的设计团队在很多事情上是正确的,这很大程度上是因为他们在自己正在干什么这个问题上付出了难以想像的思考。虽然我要为 Internet Developer(英特网开发者)书写一些脚本方面的集体思想,但我还是想去看一些过去的 Macintosh 人机界面指南,并且看看这些指导原则如何才能用到 Web 界面上,我认为这是很值当的。我很少去找可以拷贝的具体部件,而更多地寻找这个以友好闻名的界面后面隐藏的设计原则。
原则之一就是无模式。这个原则打动了我,因为它特别适合于 Web。正如 Mac 的设计者描述的那样,模式界面(在这种界面下,您能做什么取决于您当前处于什么模式下)会“把用户锁定在一个操作上,用户在该操作完成之前不允许进行其它任何操作”。使用单纯的老版本HTML时,从某种意义上看,所有界面都是模式的,进行任何修改都需要装载一个新的页面。举例来说,假定您在填充一个表单时需要一些帮助,则您必须转到包含帮助信息的新页面,然后再回到原来的页面,以完成表单的填写。
换句话说,您或者处于“帮助”模式,或者处于“表单填写”模式。这凸显了 Web 的两个主要的限制:无态(即当点击帮助连接时,您在表单中已经输入的信息将会丢失)和迟延(即您必须等待页面装载)。因此我决定写一点脚本来帮助处理这些问题。这些脚本通过动态 HTML (DHTML)技术在您点击连接时弹出一个带有帮助信息的方框。在我的演示中,就是使用它们来弹出与表单填写相关的上下文帮助信息。这些脚本也可以用在别的地方,比如弹出一篇论文的术语定义。上述的两种情况都以合理的方式给出了上下文相关的信息,即无模式的方式。同时,这个解决方案也避免了无态和迟延的问题。
实际上,我写的这些基本函数可以用在任何需要在页面上移动和改变对象可视性的地方。我做了一个快速下拉菜单的实例,就是为了演示同样这些代码的另外一种使用方式,您可能会用得到。
您可能会担心有人还在使用版本比较老的浏览器,对此,我们可以相当轻松地使这些脚本自然地回退到原来的状态,使那些使用老版本浏览器的用户可以简单地从一个单独的页面上获得信息。我将在下面的“如何使用脚本”的部分中解释如何实现这个目标。
脚本的目标
这个脚本将创建动态菜单和弹出式对象。它包括一些跨浏览器的基本函数,用于移动和改变DHTML对象的可视性。在 Netscape 4.x 中,一个 DHTML 对象是一个通过绝对位置定位的 DIV,而对 Safari,Internet Explorer 4 和 5,或者 Netscape 6 来说,则是任何 HTML 元素。这些函数可以被用在大量的 DHTML 应用中;这里还给出的两个实例,向您演示如何创建一个弹出式的提示(tip)和下拉式菜单。脚本中的主要函数如下:
changeObjectVisibility
,用来翻转一个 DHTML 对象的可视性。
moveObject
,用来在浏览器窗口中把一个 DHTML 对象移动到特定的位置上。
getStyleObject
,这个函数通过获得一个风格对象的引用简化了跨浏览器的 DTHML,我们可以从这个对象中读取属性,或者进行属性设定,包括位置,可视性,颜色,尺寸,等等。
编码的挑战
遗憾的是,由于长期以来浏览器都是由各个厂商自行实现,所以书写跨浏览器和跨平台的 DHTML 通常是拜占庭式的条件分支。为任何一个浏览器书写这些脚本都是非常轻松的;为了使它们工作在 Netscape 4 及其升级版本,以及工作在 Internet Explorer 4 及其升级版本上,事情就要复杂一些了;而使它们可以回退到比较老版本的浏览器的状态,又增加了更多的复杂度。问题在于各个浏览器在如何寻找和操作 Web 页面上的对象方面都有很多独特之处,虽然我们对这种状态已经比较熟悉了。为了方便,我写了处理这些条件分支的代码。
这些函数中有一些功能不能工作在更老一些的浏览器上,比如 Netscape 3。然而,使这些功能自然地退化并补台困难。您只需进行如下操作:
我发现的最大挑战是必须考虑浏览器处理事件的不同方式。事件发生时(比如 click 或者 mouseover 事件)光标的位置存储在一个事件对象中,而不同浏览器对事件对象的处理有轻微的不同。当事件发生时,Netscape 4 和6都产生一个新的事件对象,您可以把这个对象作为一个参数传递到函数中;而 Internet Explorer 则使用一个独立的全局 window.event
对象。对于这个问题,我在抛弃几个现在看来很草率的解决方案之后,发现把事件对象显式地传递给函数的做法可以适用于这两种浏览器:
<a href="#" onclick="showPopup('popupName',
event);">click</a>
请注意,在事件边上少了引号标识。那是因为它是一个对象,而不是文本。现在,在您的函数中就可以以如下方式使用传入的事件对象了:
function showPopup(nameOfPopup, eventObject) {
alert(eventObject.clientX);
}
一旦把对象传递给函数,您就可以通过读取 pageX
和 pageY
属性(Netscape 4 和 6)或者 clientX
和 clientY
属性(IE 4+)来获得光标的位置。然而请注意,clientX
和 clientY
属性并没有考虑页面可能被滚动的情况,因为这两个坐标是相对于窗口的左上角的,而不是整个文档。为了解决这个问题,我们加上 IE 的 document.body.scrollLeft
和 document.body.scrollTop
属性的值。如果您感兴趣的话,事件对象还有一连串有用的属性,包括一个事件触发对象的引用(在 IE 上是 srcElement
,而在 Netscape 上则是 target
)。
把事件对象作为参数进行传递的唯一麻烦是在不支持事件对象的老版本浏览器上不能工作。为了绕开这个问题,我们在 popup.js 文件中包含一个函数,该函数为那些不存在事件对象的浏览器创建一个假的对象,在装载文档时运行:
function createFakeEventObj() {
// create a fake event object for older browsers
//to avoid errors in function call when we
//need to pass the event object
if (!window.event) {
window.event = false;
}
}
这个函数把 window.event
设定为 false(假)。
这样以后,我们就可以在使用之前进行检测,看看是否存在真正的事件对象。
在 Mac 版的 Internet Explorer 5 上有一个问题,即当弹出层出现在文本的上方时,只有部分内容可以被显示。但是当我移动 DIV 标识,使之成为文档体的第一个元素时,这个问题神秘地消失了。
还是在 Mac 版的 IE 5 上,由于某些原因,document.onclick
事件只有在页面上存在实际文本时才能被触发。为了绕过这个缺陷(以便使您可以通过点击窗口中的任意位置来关闭窗口),我在页面中增加了一个不包含任何内容的,通过绝对位置定位的 DIV
,然后用 JavaScript 来改变这个 DIV 的尺寸,使之覆盖整个窗口。相关的代码大致如下:
function resizeBlankDiv() {
// resize blank placeholder div so IE 5
// on mac will get all clicks in window
if ((navigator.appVersion.indexOf('MSIE 5') != -1)
&& (navigator.platform.indexOf('Mac') != -1)
&& getStyleObject('blankDiv')) {
getStyleObject('blankDiv').width =
document.body.clientWidth - 20;
getStyleObject('blankDiv').height =
document.body.clientHeight - 20;
}
}
遗憾的是,如果浏览器的尺寸被改变了,则只有一种方法可以恢复尺寸,即重新装载整个文档(您可能认为,只要用 window.onresize
事件就可以了。然而由于这个事件在窗口的尺寸真正被改变之前就已经发生了,所以采用这种方法最终会产生不必要的滚动条)。为了恢复页面尺寸,我们又写了一个函数,在窗口尺寸被改变的任何时候,该函数可以从 Mac 平台上的 IE5 的缓存中重新装载页面。
在 Mac 版的 Internet Explorer 5 上,当您点击一个连接时,会出现一个绝对大的轮廓,这个轮廓会和将要弹出的内容相重叠。为了解决这个问题,我在连接上增加了一条风格规则:
.popupLink { outline: none }
Netscape 4 在 DIV 的命名上有一些怪异的问题。以数字开头的名称(比如“1div”),以及有些带有下划线的名称(比如“my_div”)不能转化为层,因此我通常都避免这两种情况,把我的 DIV 按类似于 myDiv 或者 div1 的形式来命名。
Netscape 4 还有一个严重的缺陷,即当窗口的尺寸被改变时,所有的风格规则都会丢失。我没有把修复这个缺陷的代码包含进来,因为已经有好几个这样的代码公布出来了,比如 Webmonke 上的这个.
最后,在 Netscape 4 中,如果您把 javascript:
放在 href
s 中,会导致页面的重新装载,并把函数的返回值当成页面的唯一内容显示出来。因此我们不应该采取下面的方式:
<a href="javascript:myFunction();">clickme</a>
而必须采取象下面的做法:
<a href="#" onclick="myFunction();
return false;">clickme</a>
实际上,这也是确保您的脚本在不能运行这些函数的浏览器上自然退化的好方法。请注意“return false”
这行代码,它使浏览器停止装载 href
参数指定的URL。这样,如果浏览器中 JavaScript 被关闭,或者浏览器不能处理 JavaScript,则您可以提供一个不同的页面;但是如果这里的函数可以运行,则连接不会被打开。
在这个演示中,我们讨论的更深一些:
<a href="#" onclick="return
!showPopup('nameFieldPopup', event);">
clickme</a>
我们不去深入到所有的细节,只是大概看看这行代码,它的意思是运行 showPopup
函数,然后返回该函数返回值的非。那样,如果 showPopup
返回 true
(意思是它成功显示了弹出层),我们就把 false
返回给连接,这样连接就不会改变页面。另外一方面,如果 showPopup
返回 false(意思是它不能显示弹出层),则我们就继续执行脚本,跟着连接进入到一个独立的页面,该页面具有和弹出层相同的信息。这个逻辑看起来可能有点混淆,但是只要记住一条就可以了:如果您返回 false,
连接就不起作用了。
使用脚本
如果要使用这些脚本来实现弹出机制,请按照如下这些步骤来进行:
- 如果要进行层的弹出,则需要把层工具和实现弹出机制的脚本文件都包含进您的页面。这可以通过把下面两条语句包含到您的文档头部来实现:
<script src="utility.js"></script>
<script src="popup.js"></script>
- 确保有可以被弹出的
DIV
。这些 DIV 必须被绝对定位,并且在开始是应该被隐藏。例如:
<DIV onclick="event.cancelBubble = true;"
class=popup id=nameOfPopup>
Popup text goes here.<br>
<a href="#" onclick="hideCurrentPopup();
return false;">
You can include a link like this to
close the DIV if you like
</a>
</DIV>
确保在DIV中包含onclick="event.cancelBubble = true;"
这行代码。它告诉JavaScript在您点击DIV
时不要把点击事件传递给页面中的其它对象。如果省略这行代码,则弹出层在被点击时就会关闭(对于大多数浏览器来说),因为我们已经设定了一个关闭弹出层的事件处理函数。把这行代码包含到页面中的基本目的是告诉浏览器“当人们点击除了弹出层自身(或者打开弹出层的原始连接)之外的任何地方时,关闭弹出层”。
- 如果要改变弹出层的外观,请编辑风格表单中的
.popup
的风格规则。
- 在每一个应该触发弹出层的地方调用
showPopup
函数,把nameOfPopup
改为您希望显示的弹出层名称(但是把它放在单引号中):
<a href="http://url.for.older.browsers"
onclick="return !showPopup
('nameOfPopup', event);">
clickme</a>
如果您希望当鼠标在连接上滚动时出发弹出层,则只要修改触发事件就可以了:
<a href="http://url.for.older.browsers"
onmouseover="showPopup('nameOfPopup', event);"
onmouseout="hideCurrentPopup();">clickme
</a>
- (可选)修改
popup.js
文件中的两个变量,这两个变量用来控制弹出层出现的位置,该位置是相对于当前光标位置的:
var xOffset = 30;
var yOffset = -5;
下面对相关的函数逐一进行说明:
changeObjectVisibility(objectId, newVisibility)
:调用这个函数时,objectId
应该是您希望显示或者隐藏的对象名称。函数希望这个参数是文本类型的,因此您需要把它包含在引号中。newVisibility
参数的值或者是 visible(可视)
或者是 hidden(隐藏)
。再次说明一下,这个值是一个字符串类型的,因此需要把它包含在引号中。下面这个实例把一个名为 myBigLayer
: 的对象隐藏起来:
changeObjectVisibility('myBigLayer', 'hidden')
moveObject(objectId, newXCoordinate, newYCoordinate)
:同样的,objectId
应该是您希望移动的对象名称。它是一个文本类型的参数,因此应该放在引号里面。newXCoordinate
和 newYCoordinate
a 是数字类型的(因此没有引号),描述您希望把对象移动到什么地方。因此,如果要把 myBigLayer
对象移动到距离窗口左边 300 p 像素,距离窗口上边10像素的位置,书写如下代码就可以了:
moveObject('myBigLayer', 300, 10)
getStyleObject(objectId)
:上述两个函数都使用这个函数来把对象的名称转变为属于该对象的风格对象的引用。对于 Netscape 4+ 和 IE 4+ 两款浏览器来说,这个函数都能返回正确的引用,因此您不必担心浏览器在工作方式上的差别。(请注意:有一种情况在 Netscape 4 上处理不了,那就是聚集层,因此您必须避免把层放到其它层上)。
在您需要改变对象的 CSS 属性的任何时候,您都可以脱离这里描述的上下文来使用这个函数。例如,假定我们要给 myBigLayer
设定一个绿的背景色,可以书写如下代码:
ar myBigLayerStyleObject =
getStyleObject('myBigLayer');
myBigLayerStyleObject.backgroundColor =
'green';
Or, for shorthand, you could just do this:
getStyleObject('myBigLayer').backgroundColor
= 'green';
utility.txt
// Copyright ?2000 by Apple Computer, Inc., All Rights Reserved.
//
// You may incorporate this Apple sample code into your own code
// without restriction. This Apple sample code has been provided "AS IS"
// and the responsibility for its operation is yours. You may redistribute
// this code, but you are not permitted to redistribute it as
// "Apple sample code" after having made changes.
//
// ************************
// layer utility routines *
// ************************
function getStyleObject(objectId) {
// cross-browser function to get an object's style object given its id
if(document.getElementById && document.getElementById(objectId)) {
// W3C DOM
return document.getElementById(objectId).style;
} else if (document.all && document.all(objectId)) {
// MSIE 4 DOM
return document.all(objectId).style;
} else if (document.layers && document.layers[objectId]) {
// NN 4 DOM.. note: this won't find nested layers
return document.layers[objectId];
} else {
return false;
}
} // getStyleObject
function changeObjectVisibility(objectId, newVisibility) {
// get a reference to the cross-browser style object and make sure the object exists
var styleObject = getStyleObject(objectId);
if(styleObject) {
styleObject.visibility = newVisibility;
return true;
} else {
// we couldn't find the object, so we can't change its visibility
return false;
}
} // changeObjectVisibility
function moveObject(objectId, newXCoordinate, newYCoordinate) {
// get a reference to the cross-browser style object and make sure the object exists
var styleObject = getStyleObject(objectId);
if(styleObject) {
styleObject.left = newXCoordinate;
styleObject.top = newYCoordinate;
return true;
} else {
// we couldn't find the object, so we can't very well move it
return false;
}
} // moveObject
popup.txt
// Copyright ?2000 by Apple Computer, Inc., All Rights Reserved.
//
// You may incorporate this Apple sample code into your own code
// without restriction. This Apple sample code has been provided "AS IS"
// and the responsibility for its operation is yours. You may redistribute
// this code, but you are not permitted to redistribute it as
// "Apple sample code" after having made changes.
// ********************************
// application-specific functions *
// ********************************
// store variables to control where the popup will appear relative to the cursor position
// positive numbers are below and to the right of the cursor, negative numbers are above and to the left
var xOffset = 30;
var yOffset = -5;
function showPopup (targetObjectId, eventObj) {
if(eventObj) {
// hide any currently-visible popups
hideCurrentPopup();
// stop event from bubbling up any farther
eventObj.cancelBubble = true;
// move popup div to current cursor position
// (add scrollTop to account for scrolling for IE)
var newXCoordinate = (eventObj.pageX)?eventObj.pageX + xOffset:eventObj.x + xOffset + ((document.body.scrollLeft)?document.body.scrollLeft:0);
var newYCoordinate = (eventObj.pageY)?eventObj.pageY + yOffset:eventObj.y + yOffset + ((document.body.scrollTop)?document.body.scrollTop:0);
moveObject(targetObjectId, newXCoordinate, newYCoordinate);
// and make it visible
if( changeObjectVisibility(targetObjectId, 'visible') ) {
// if we successfully showed the popup
// store its Id on a globally-accessible object
window.currentlyVisiblePopup = targetObjectId;
return true;
} else {
// we couldn't show the popup, boo hoo!
return false;
}
} else {
// there was no event object, so we won't be able to position anything, so give up
return false;
}
} // showPopup
function hideCurrentPopup() {
// note: we've stored the currently-visible popup on the global object window.currentlyVisiblePopup
if(window.currentlyVisiblePopup) {
changeObjectVisibility(window.currentlyVisiblePopup, 'hidden');
window.currentlyVisiblePopup = false;
}
} // hideCurrentPopup
// ***********************
// hacks and workarounds *
// ***********************
// initialize hacks whenever the page loads
window.onload = initializeHacks;
// setup an event handler to hide popups for generic clicks on the document
document.onclick = hideCurrentPopup;
function initializeHacks() {
// this ugly little hack resizes a blank div to make sure you can click
// anywhere in the window for Mac MSIE 5
if ((navigator.appVersion.indexOf('MSIE 5') != -1)
&& (navigator.platform.indexOf('Mac') != -1)
&& getStyleObject('blankDiv')) {
window.onresize = explorerMacResizeFix;
}
resizeBlankDiv();
// this next function creates a placeholder object for older browsers
createFakeEventObj();
}
function createFakeEventObj() {
// create a fake event object for older browsers to avoid errors in function call
// when we need to pass the event object to functions
if (!window.event) {
window.event = false;
}
} // createFakeEventObj
function resizeBlankDiv() {
// resize blank placeholder div so IE 5 on mac will get all clicks in window
if ((navigator.appVersion.indexOf('MSIE 5') != -1)
&& (navigator.platform.indexOf('Mac') != -1)
&& getStyleObject('blankDiv')) {
getStyleObject('blankDiv').width = document.body.clientWidth - 20;
getStyleObject('blankDiv').height = document.body.clientHeight - 20;
}
}
function explorerMacResizeFix () {
location.reload(false);
}
弹出式帮助的实例
<HTML><HEAD>
<script src="utility.txt"></script>
<script src="popup.txt"></script>
<STYLE>
.popupLink { COLOR: red; outline: none }
.popup { POSITION: absolute; VISIBILITY: hidden; BACKGROUND-COLOR: yellow; LAYER-BACKGROUND-COLOR: yellow; width: 200; BORDER-LEFT: 1px solid black; BORDER-TOP: 1px solid black; BORDER-BOTTOM: 3px solid black; BORDER-RIGHT: 3px solid black; PADDING: 3px; z-index: 10 }
</STYLE>
<BODY bgcolor="#ffffff">
<!-- keep the popup divs as the first things on the page or else MSIE 5 on the mac sometimes has trouble rendering them on top of text -->
<DIV onclick='event.cancelBubble = true;' class=popup id=nameFieldPopup>Hi, [your name here]! We need to know your <b>name</b> so we can address you a bit more personally. [<a class=closeLink href='#' onclick='hideCurrentPopup(); return false;'>close this tip</a>]</DIV>
<DIV onclick='event.cancelBubble = true;' class=popup id=emailFieldPopup>Well, yeah, you could put in a fake <b>email address</b>, but then we couldn't send you occasional updates. Oh and, um, we promise not to spam you. [<a class=closeLink href='#' onclick='hideCurrentPopup(); return false;'>close this tip</a>]</DIV>
<!-- begin body of document -->
<form>
<p>Fill in the form:</p>
<P>Name: <input type=text> [<a href="non_js_help.html" class=popupLink onclick="return !showPopup('nameFieldPopup', event);">help</a>]</P>
<P>Email: <input type=text> [<a href="non_js_help.html" class=popupLink onclick="return !showPopup('emailFieldPopup', event);">help</a>]</P>
</form>
<!-- leave this blank div in here to make sure you can click anywhere on the document for MSIE 5 mac -->
<div id="blankDiv" style="position: absolute; left: 0; top: 0; visibility: hidden"></div>
</BODY></HTML>
http://www.apple.com.cn/developer/internet/webcontent/hideshow_layer.html