有些事情現在已經很容易被忘記了,但是在開發原創的 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
onclick="return !showPopup
('nameOfPopup', event);">
clickme</a>
如果您希望當鼠標在連接上滾動時出發彈出層,則只要修改觸發事件就可以了:
<a
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