有些事情現(xiàn)在已經(jīng)很容易被忘記了,但是在開發(fā)原創(chuàng)的 Mac OS 的時候,業(yè)界到處出現(xiàn)新的圖形用戶接口(GUI),人們所做的工作差別相當(dāng)小。Macintosh 的設(shè)計團(tuán)隊在很多事情上是正確的,這很大程度上是因為他們在自己正在干什么這個問題上付出了難以想像的思考。雖然我要為 Internet Developer(英特網(wǎng)開發(fā)者)書寫一些腳本方面的集體思想,但我還是想去看一些過去的 Macintosh 人機界面指南,并且看看這些指導(dǎo)原則如何才能用到 Web 界面上,我認(rèn)為這是很值當(dāng)?shù)摹N液苌偃フ铱梢钥截惖木唧w部件,而更多地尋找這個以友好聞名的界面后面隱藏的設(shè)計原則。
原則之一就是無模式。這個原則打動了我,因為它特別適合于 Web。正如 Mac 的設(shè)計者描述的那樣,模式界面(在這種界面下,您能做什么取決于您當(dāng)前處于什么模式下)會“把用戶鎖定在一個操作上,用戶在該操作完成之前不允許進(jìn)行其它任何操作”。使用單純的老版本HTML時,從某種意義上看,所有界面都是模式的,進(jìn)行任何修改都需要裝載一個新的頁面。舉例來說,假定您在填充一個表單時需要一些幫助,則您必須轉(zhuǎn)到包含幫助信息的新頁面,然后再回到原來的頁面,以完成表單的填寫。
換句話說,您或者處于“幫助”模式,或者處于“表單填寫”模式。這凸顯了 Web 的兩個主要的限制:無態(tài)(即當(dāng)點擊幫助連接時,您在表單中已經(jīng)輸入的信息將會丟失)和遲延(即您必須等待頁面裝載)。因此我決定寫一點腳本來幫助處理這些問題。這些腳本通過動態(tài) HTML (DHTML)技術(shù)在您點擊連接時彈出一個帶有幫助信息的方框。在我的演示中,就是使用它們來彈出與表單填寫相關(guān)的上下文幫助信息。這些腳本也可以用在別的地方,比如彈出一篇論文的術(shù)語定義。上述的兩種情況都以合理的方式給出了上下文相關(guān)的信息,即無模式的方式。同時,這個解決方案也避免了無態(tài)和遲延的問題。
實際上,我寫的這些基本函數(shù)可以用在任何需要在頁面上移動和改變對象可視性的地方。我做了一個快速下拉菜單的實例,就是為了演示同樣這些代碼的另外一種使用方式,您可能會用得到。
您可能會擔(dān)心有人還在使用版本比較老的瀏覽器,對此,我們可以相當(dāng)輕松地使這些腳本自然地回退到原來的狀態(tài),使那些使用老版本瀏覽器的用戶可以簡單地從一個單獨的頁面上獲得信息。我將在下面的“如何使用腳本”的部分中解釋如何實現(xiàn)這個目標(biāo)。
腳本的目標(biāo)
這個腳本將創(chuàng)建動態(tài)菜單和彈出式對象。它包括一些跨瀏覽器的基本函數(shù),用于移動和改變DHTML對象的可視性。在 Netscape 4.x 中,一個 DHTML 對象是一個通過絕對位置定位的 DIV,而對 Safari,Internet Explorer 4 和 5,或者 Netscape 6 來說,則是任何 HTML 元素。這些函數(shù)可以被用在大量的 DHTML 應(yīng)用中;這里還給出的兩個實例,向您演示如何創(chuàng)建一個彈出式的提示(tip)和下拉式菜單。腳本中的主要函數(shù)如下:
changeObjectVisibility
,用來翻轉(zhuǎn)一個 DHTML 對象的可視性。moveObject
,用來在瀏覽器窗口中把一個 DHTML 對象移動到特定的位置上。getStyleObject
,這個函數(shù)通過獲得一個風(fēng)格對象的引用簡化了跨瀏覽器的 DTHML,我們可以從這個對象中讀取屬性,或者進(jìn)行屬性設(shè)定,包括位置,可視性,顏色,尺寸,等等。
編碼的挑戰(zhàn)
遺憾的是,由于長期以來瀏覽器都是由各個廠商自行實現(xiàn),所以書寫跨瀏覽器和跨平臺的 DHTML 通常是拜占庭式的條件分支。為任何一個瀏覽器書寫這些腳本都是非常輕松的;為了使它們工作在 Netscape 4 及其升級版本,以及工作在 Internet Explorer 4 及其升級版本上,事情就要復(fù)雜一些了;而使它們可以回退到比較老版本的瀏覽器的狀態(tài),又增加了更多的復(fù)雜度。問題在于各個瀏覽器在如何尋找和操作 Web 頁面上的對象方面都有很多獨特之處,雖然我們對這種狀態(tài)已經(jīng)比較熟悉了。為了方便,我寫了處理這些條件分支的代碼。
這些函數(shù)中有一些功能不能工作在更老一些的瀏覽器上,比如 Netscape 3。然而,使這些功能自然地退化并補臺困難。您只需進(jìn)行如下操作:
- 直接把這些腳本包含在頁面上,而不是使用連接的 .js 文件。
- 只在提供相關(guān)支持的瀏覽器上使用 JavaScript 進(jìn)行彈出式的
DIV 的
輸出。實現(xiàn)這個控制的代碼大致如下:if(document.getElementById || document.all || document.layers) { // write out div tag with document.write }
我發(fā)現(xiàn)的最大挑戰(zhàn)是必須考慮瀏覽器處理事件的不同方式。事件發(fā)生時(比如 click 或者 mouseover 事件)光標(biāo)的位置存儲在一個事件對象中,而不同瀏覽器對事件對象的處理有輕微的不同。當(dāng)事件發(fā)生時,Netscape 4 和6都產(chǎn)生一個新的事件對象,您可以把這個對象作為一個參數(shù)傳遞到函數(shù)中;而 Internet Explorer 則使用一個獨立的全局 window.event
對象。對于這個問題,我在拋棄幾個現(xiàn)在看來很草率的解決方案之后,發(fā)現(xiàn)把事件對象顯式地傳遞給函數(shù)的做法可以適用于這兩種瀏覽器:
<a href="#" onclick="showPopup('popupName',
event);">click</a>
請注意,在事件邊上少了引號標(biāo)識。那是因為它是一個對象,而不是文本。現(xiàn)在,在您的函數(shù)中就可以以如下方式使用傳入的事件對象了:
function showPopup(nameOfPopup, eventObject) {
alert(eventObject.clientX);
}
一旦把對象傳遞給函數(shù),您就可以通過讀取 pageX
和 pageY
屬性(Netscape 4 和 6)或者 clientX
和 clientY
屬性(IE 4+)來獲得光標(biāo)的位置。然而請注意,clientX
和 clientY
屬性并沒有考慮頁面可能被滾動的情況,因為這兩個坐標(biāo)是相對于窗口的左上角的,而不是整個文檔。為了解決這個問題,我們加上 IE 的 document.body.scrollLeft
和 document.body.scrollTop
屬性的值。如果您感興趣的話,事件對象還有一連串有用的屬性,包括一個事件觸發(fā)對象的引用(在 IE 上是 srcElement
,而在 Netscape 上則是 target
)。
把事件對象作為參數(shù)進(jìn)行傳遞的唯一麻煩是在不支持事件對象的老版本瀏覽器上不能工作。為了繞開這個問題,我們在 popup.js 文件中包含一個函數(shù),該函數(shù)為那些不存在事件對象的瀏覽器創(chuàng)建一個假的對象,在裝載文檔時運行:
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;
}
}
這個函數(shù)把 window.event
設(shè)定為 false(假)。
這樣以后,我們就可以在使用之前進(jìn)行檢測,看看是否存在真正的事件對象。
在 Mac 版的 Internet Explorer 5 上有一個問題,即當(dāng)彈出層出現(xiàn)在文本的上方時,只有部分內(nèi)容可以被顯示。但是當(dāng)我移動 DIV 標(biāo)識,使之成為文檔體的第一個元素時,這個問題神秘地消失了。
還是在 Mac 版的 IE 5 上,由于某些原因,document.onclick
事件只有在頁面上存在實際文本時才能被觸發(fā)。為了繞過這個缺陷(以便使您可以通過點擊窗口中的任意位置來關(guān)閉窗口),我在頁面中增加了一個不包含任何內(nèi)容的,通過絕對位置定位的 DIV
,然后用 JavaScript 來改變這個 DIV 的尺寸,使之覆蓋整個窗口。相關(guān)的代碼大致如下:
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;
}
}
遺憾的是,如果瀏覽器的尺寸被改變了,則只有一種方法可以恢復(fù)尺寸,即重新裝載整個文檔(您可能認(rèn)為,只要用 window.onresize
事件就可以了。然而由于這個事件在窗口的尺寸真正被改變之前就已經(jīng)發(fā)生了,所以采用這種方法最終會產(chǎn)生不必要的滾動條)。為了恢復(fù)頁面尺寸,我們又寫了一個函數(shù),在窗口尺寸被改變的任何時候,該函數(shù)可以從 Mac 平臺上的 IE5 的緩存中重新裝載頁面。
在 Mac 版的 Internet Explorer 5 上,當(dāng)您點擊一個連接時,會出現(xiàn)一個絕對大的輪廓,這個輪廓會和將要彈出的內(nèi)容相重疊。為了解決這個問題,我在連接上增加了一條風(fēng)格規(guī)則:
.popupLink { outline: none }
Netscape 4 在 DIV 的命名上有一些怪異的問題。以數(shù)字開頭的名稱(比如“1div”),以及有些帶有下劃線的名稱(比如“my_div”)不能轉(zhuǎn)化為層,因此我通常都避免這兩種情況,把我的 DIV 按類似于 myDiv 或者 div1 的形式來命名。
Netscape 4 還有一個嚴(yán)重的缺陷,即當(dāng)窗口的尺寸被改變時,所有的風(fēng)格規(guī)則都會丟失。我沒有把修復(fù)這個缺陷的代碼包含進(jìn)來,因為已經(jīng)有好幾個這樣的代碼公布出來了,比如 Webmonke 上的這個.
最后,在 Netscape 4 中,如果您把 javascript:
放在 href
s 中,會導(dǎo)致頁面的重新裝載,并把函數(shù)的返回值當(dāng)成頁面的唯一內(nèi)容顯示出來。因此我們不應(yīng)該采取下面的方式:
<a href="javascript:myFunction();">clickme</a>
而必須采取象下面的做法:
<a href="#" onclick="myFunction();
return false;">clickme</a>
實際上,這也是確保您的腳本在不能運行這些函數(shù)的瀏覽器上自然退化的好方法。請注意“return false”
這行代碼,它使瀏覽器停止裝載 href
參數(shù)指定的URL。這樣,如果瀏覽器中 JavaScript 被關(guān)閉,或者瀏覽器不能處理 JavaScript,則您可以提供一個不同的頁面;但是如果這里的函數(shù)可以運行,則連接不會被打開。
在這個演示中,我們討論的更深一些:
<a href="#" onclick="return
!showPopup('nameFieldPopup', event);">
clickme</a>
我們不去深入到所有的細(xì)節(jié),只是大概看看這行代碼,它的意思是運行 showPopup
函數(shù),然后返回該函數(shù)返回值的非。那樣,如果 showPopup
返回 true
(意思是它成功顯示了彈出層),我們就把 false
返回給連接,這樣連接就不會改變頁面。另外一方面,如果 showPopup
返回 false(意思是它不能顯示彈出層),則我們就繼續(xù)執(zhí)行腳本,跟著連接進(jìn)入到一個獨立的頁面,該頁面具有和彈出層相同的信息。這個邏輯看起來可能有點混淆,但是只要記住一條就可以了:如果您返回 false,
連接就不起作用了。
使用腳本
如果要使用這些腳本來實現(xiàn)彈出機制,請按照如下這些步驟來進(jìn)行:
- 如果要進(jìn)行層的彈出,則需要把層工具和實現(xiàn)彈出機制的腳本文件都包含進(jìn)您的頁面。這可以通過把下面兩條語句包含到您的文檔頭部來實現(xiàn):
<script src="utility.js"></script> <script src="popup.js"></script>
- 確保有可以被彈出的
DIV
。這些 DIV 必須被絕對定位,并且在開始是應(yīng)該被隱藏。例如:<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
時不要把點擊事件傳遞給頁面中的其它對象。如果省略這行代碼,則彈出層在被點擊時就會關(guān)閉(對于大多數(shù)瀏覽器來說),因為我們已經(jīng)設(shè)定了一個關(guān)閉彈出層的事件處理函數(shù)。把這行代碼包含到頁面中的基本目的是告訴瀏覽器“當(dāng)人們點擊除了彈出層自身(或者打開彈出層的原始連接)之外的任何地方時,關(guān)閉彈出層”。 - 如果要改變彈出層的外觀,請編輯風(fēng)格表單中的
.popup
的風(fēng)格規(guī)則。 - 在每一個應(yīng)該觸發(fā)彈出層的地方調(diào)用
showPopup
函數(shù),把nameOfPopup
改為您希望顯示的彈出層名稱(但是把它放在單引號中):<a onclick="return !showPopup ('nameOfPopup', event);"> clickme</a>
如果您希望當(dāng)鼠標(biāo)在連接上滾動時出發(fā)彈出層,則只要修改觸發(fā)事件就可以了:<a onmouseover="showPopup('nameOfPopup', event);" onmouseout="hideCurrentPopup();">clickme </a>
- (可選)修改
popup.js
文件中的兩個變量,這兩個變量用來控制彈出層出現(xiàn)的位置,該位置是相對于當(dāng)前光標(biāo)位置的:var xOffset = 30; var yOffset = -5;
下面對相關(guān)的函數(shù)逐一進(jìn)行說明:
changeObjectVisibility(objectId, newVisibility)
:調(diào)用這個函數(shù)時,objectId
應(yīng)該是您希望顯示或者隱藏的對象名稱。函數(shù)希望這個參數(shù)是文本類型的,因此您需要把它包含在引號中。newVisibility
參數(shù)的值或者是visible(可視)
或者是hidden(隱藏)
。再次說明一下,這個值是一個字符串類型的,因此需要把它包含在引號中。下面這個實例把一個名為myBigLayer
: 的對象隱藏起來:changeObjectVisibility('myBigLayer', 'hidden')
moveObject(objectId, newXCoordinate, newYCoordinate)
:同樣的,objectId
應(yīng)該是您希望移動的對象名稱。它是一個文本類型的參數(shù),因此應(yīng)該放在引號里面。newXCoordinate
和newYCoordinate
a 是數(shù)字類型的(因此沒有引號),描述您希望把對象移動到什么地方。因此,如果要把myBigLayer
對象移動到距離窗口左邊 300 p 像素,距離窗口上邊10像素的位置,書寫如下代碼就可以了:moveObject('myBigLayer', 300, 10)
getStyleObject(objectId)
:上述兩個函數(shù)都使用這個函數(shù)來把對象的名稱轉(zhuǎn)變?yōu)閷儆谠搶ο蟮娘L(fēng)格對象的引用。對于 Netscape 4+ 和 IE 4+ 兩款瀏覽器來說,這個函數(shù)都能返回正確的引用,因此您不必?fù)?dān)心瀏覽器在工作方式上的差別。(請注意:有一種情況在 Netscape 4 上處理不了,那就是聚集層,因此您必須避免把層放到其它層上)。在您需要改變對象的 CSS 屬性的任何時候,您都可以脫離這里描述的上下文來使用這個函數(shù)。例如,假定我們要給
myBigLayer
設(shè)定一個綠的背景色,可以書寫如下代碼: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