2009年01月15日
[JavaScript] 如何讓 reset.css 和 HTML 預覽功能並存?
問題描述
現在 CSS 排版技術越來越盛行,很多網頁設計師為更精準地控制頁面呈現效果,都會使用 reset.css 來將 HTML 元素在瀏覽器的預設呈現效果給移除 (例如 YUI 的 reset.css 或 Eric Meyer 的 reset.css ) 。
雖然這個技術很好用,但在需要讓客戶自訂畫面的專案上就會遇到一個問題:如何讓客戶的 HTML 呈現出瀏覽器原本預設的效果?
像 YUI 有提供一個 base.css ,可以稍微幫我們將 HTML 元素回復原本的呈現方式。但我們有時只是需要在畫面上一小部份轉換為原來的效果,這時就勢必得修改 base.css ,在它的各項元素定義前加上某區塊的 class 或 id ;不過這不是一個好選項,因為大多數狀況我們還是沒辦法百分之百讓畫面呈現它原本的樣子。
解決方案
其實用 iframe 就可以幫我們解決這個問題。
步驟很簡單:
- 建立一個動態 iframe 。
- 將原本的 html 內容指定給 iframe 的 document 。
- 把 iframe 高度調整至和 html 內容一般高。
這裡我用 jQuery 來實作囉。
建立一個動態 iframe
假設我們的 html 內容已經放在 div.htmlContent 這個區塊中,我們可以用以下程式來動態建立一個 iframe ,並將它 append 在 div.htmlContent 中。當然也別忘記把原來的內容清掉:
$(function () {
var $iframe = $('<iframe src="about:blank" width="100%" height="300" frameborder="0"></iframe>');
var $htmlContent = $('div.htmlContent');
var html = $htmlContent.html();
$htmlContent.html('');
$htmlContent.append($iframe);
}
這裡要注意一點,就是 iframe 的高度一定要設定一個大於 0 的整數值,不然等一下 IE 在自動調整高度時會出問題。 (別問我為什麼...我也是自己試出來的 Orz)
至於 iframe 的來源,我們設定為 about:blank 就好,也就是瀏覽器預設的空白頁。
將原本的 html 內容指定給 iframe 的 document
在建立好動態的 iframe 之後,我們要先等它將內容載完。在載完之後,我們就可以將原來的 html 放到 iframe 裡的 docuement 物件的 body 中。
$iframe.load(function () {
var iframeDocument = getIframeDocument($iframe.get(0));
$(iframeDocument.body).html(html).css({ margin: '0', padding: '0' });
});
這裡用到了一個自訂函式 getIframeDocument , 它可以幫我們取得 iframe 裡的 document 物件,而且是跨瀏覽器的:
function getIframeDocument(iframeObject) {
if (iframeObject.contentWindow) {
return iframeObject.contentWindow.document;
} else if (iframeObject.contentDocument) {
return iframeObject.contentDocument.document;
}
return null;
}
註:getIframeDocument 暫時不支援 Google Chrome 和 Safari 。
把 iframe 高度調整至和 html 內容一般高
接著我們要調整 iframe 的高度,讓它能完整呈現出 html 的內容。
在 html 載入完畢後,body 的 offsetHeight 就會是我們所需要的內容高度:
$iframe.load(function () {
var iframeDocument = $.wacow.getIframeDocument($iframe.get(0));
$(iframeDocument.body).html(html).css({ margin: '0', padding: '0' });
setTimeout(function () {
var bodyHeight = iframeDocument.body.offsetHeight + 20;
$iframe.height(bodyHeight);
}, 0);
});
這裡有個小技巧,就是重新指定 html 內容時, load 事件並不會被觸發,所以我們要利用 setTimeout 來確定 html 已經全部加載完畢 (讓它跳離到另一個執行空間) 。
到這裡就大功告成啦~謝謝收看~
2008年12月3日
[JavaScript] Firefox 在 DHTML 解析上的問題
昨天小魚在工作遇到了一個 JavaScript 的問題來請教我,基於對技術的興趣我就試著幫她解決;經過一番研究後,終於被我找到原因。
問題描述
小魚在整合其他人的舊程式時,測試到一支要產生動態 select 選項的程式。而這支舊程式的寫法如下:
-
先利用傳統的 Ajax (XMLHttpRequest) 取得 PHP 輸出的一段 HTML 表單 select 物件,然後利用 innerHTML 放在前端的 POST 表單裡。
-
接著在新 select 選項中選擇一個項目後,按下送出。
但神奇的是,小魚執行時發現 IE 是可以送出 POST 資料,但 Firefox 卻不行。
實驗過程
我簡單地用以下程式模擬,這是前端頁面 (index.php) :
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title> AJAX 測試</title>
</head>
<body>
<script type="text/javascript">
function createXMLHttpRequest() {
if (window.XMLHttpRequest) {
return new XMLHttpRequest();
}
else if (window.ActiveXObject) {
return new ActiveXObject("Microsoft.XMLHTTP");
}
}
function handler() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById('result').innerHTML = this.responseText;
}
}
var xmlHttp = createXMLHttpRequest();
function run() {
xmlHttp.open('POST', 'test.php');
xmlHttp.onreadystatechange = handler;
xmlHttp.send(null);
}
</script>
<form action="" method="post">
<div id="result">
</div>
<input type="submit" value="送出" />
</form>
<input type="button" id="test" value="測試" />
<hr />
<?php var_dump($_POST); ?>
<script type="text/javascript">
document.getElementById('test').onclick = run;
</script>
</body>
</html>
接著是後端 PHP 程式 (test.php) :
<?php
header('Content-Type: text/html');
?>
<select name="test">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
我測試的結果並沒有發現小魚所說的狀況,不論是 IE 或 Firefox 都可以正常傳送出 POST 資料。
然後我想說會不會是 Ajax 的關係,所以我建議小魚使用 jQuery 的 ajax 方法試試,但結果還是相同。
後來我再仔細再看了看她的 HTML 碼,發現 form 標籤是藏在 tr 標籤裡,因此我把表單修改如下:
<table>
<tr><td>測試</td></tr>
<form action="" method="post">
<tr>
<td>
<div id="result">
</div>
<input type="submit" value="送出" />
</td>
</tr>
</form>
<input type="button" id="test" value="測試" />
</table>
然後再測試一次,就發現 Firefox 真的無法送出 POST 資料了。
接著我想是不是只有 select 有這個問題,所以我又在表單內再加了一個靜態的 input 元件:
<table>
<tr><td>測試</td></tr>
<form action="" method="post">
<tr>
<td>
<div id="result">
</div>
<input type="text" name="var1" value="temp1" />
<input type="submit" value="送出" />
</td>
</tr>
</form>
<input type="button" id="test" value="測試" />
</table>
而 Ajax 取得的 PHP 輸出內容也加了一個 input 元件:
<?php
header('Content-Type: text/html');
?>
<select name="test">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
<input type="text" name="var2" value="temp2" />
再次試驗的結果,發現只要是 Ajax 丟出來的動態表單元件,雖然已經載入在 DOM 裡 (可以 Firebug 觀察) ,但實際 Submit 時則無法被 Firefox 所接受 (但畫面上的確會出現該元件) 。
然後我把 form 標籤移到正常的 table 外後,測試的結果一切正常,不論 IE 或 Firefox 都能正確送出 POST 資料。
結論
從上面的結果,我推測 Firefox 在處理動態內容時,應該是遵守著標準的 HTML 規範;所以不正確的 form 標籤位置會使得 Firefox 在處理動態元件時有錯誤,導致實際 Submit 表單時,這些元件就被 Firefox 給忽略掉了。
當然 Firefox 底層是怎麼處理的,我並沒有深入研究,所以這個結論可能是有誤的。但是從這次的問題的解決方法來看,只要保持正確的 HTML 結構,我想應該就能避開很多瀏覽器差異上的問題了。
註:只是苦了小魚,她說她有一堆別人寫的舊程式碼都是這種寫法。
2008年11月17日
[JavaScript] 複製物件
前幾天遇到了一個 JavaScript 的小問題,就是物件的複製。
這個問題主要是我先設定了一個全域變數,然後在函式裡去重新定義一個變數,並將全域變數的內容指定給新變數。
我以為這樣就是「複製了 JavaScript 的物件」,但事實上是錯的。
我特地上網找了一下,發現 JavaScript 本身並沒有提供比較方便的 clone 機制,這時我的腦筋就動到 jQuery 上了。
不過這裡我可不是說 jQuery 的 clone 方法,而是 extend 方法。
先來看看例子好了:
<html>
<head>
<script type="text/javascript" src="jquery/1.2.6.js"></script>
<script type="text/javascript">
function w(s) {
document.write(s + '<br />\n');
}
function d(o) {
var s = '';
for (var i in o) {
s += i + ': ' + o[i] + '<br />\n';
}
w(s);
}
</script>
</head>
<body>
<script type="text/javascript">
var o1 = {
a1: '123',
a2: '456'
};
var o2 = o1;
$.extend(o2, { a3: '789' });
var o3 = $.extend({}, o1);
$.extend(o3, { a4: '000' });
w('o1');
d(o1);
w('o2');
d(o2);
w('o3');
d(o3);
</script>
</body>
</html>
在上面的程式中,請將重點放在我強調的部份。
這裡我先定義一個自訂物件 o1 ,然後我將 o2 指定為 o1 ;在 JavaScript 的意義裡, o2 就會是 o1 的「別名」,兩個都指到同一個物件。
因此接下來我對 o2 進行任何操作,都會影響到 o1 ;也就是說如果我們要複製 o1 的話,就不能用等號 (=) 。
jQuery 的 extend 方法可以幫我們這個忙。
原因是 extend 會將第二個參數裡的物件成員,一項一項地複製到第一個參數上。因此我們可以用它來解決 JavaScript 複製物件的問題。
在 o1 複製到 o3 中,很重要的一個關鍵就是我們需要把 $.extend 的第一個參數設為空物件;這是因為 $.extend 會回傳第一個參數,我們就省掉先行定義 o3 為空物件的動作了。
接下來我們不論怎麼對 o3 進行處理,也不會影響到 o1 ;換句話說,我們已經成功達成 clone JavaScript Object 的目標啦。
2008年09月23日
[jQuery] IE 上的 clone 陷阱
前陣子在處理客戶更改版面的需求時,為了偷懶,結果發現了一個 jQuery 在 IE 上 clone 元素的問題。
先簡單說明一下例子:

如上圖所示,我希望在按下「複製」按鈕後,藍色區塊中的 checkbox 被勾選的項目會被複製到紅色區塊中:

這裡我簡單的使用 jQuery 的 clone 方法來完成它,原始程式如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>測試</title>
<script type="text/javascript" src="jquery/1.2.6.js"></script>
<script type="text/javascript">
$(function () {
$('#copy').click(function () {
$('#target').html('').append($('input.test:checked').clone());
});
});
</script>
</head>
<body>
<div id="source" style="border:1px solid #33F; padding: 20px; width: 200px;">
<input type="checkbox" name="test[]" class="test" value="1" />
<input type="checkbox" name="test[]" class="test" value="2" />
<input type="checkbox" name="test[]" class="test" value="3" />
<input type="checkbox" name="test[]" class="test" value="4" />
</div>
<br />
<div id="target" style="border:1px solid #F33; padding: 20px; width: 200px;">
</div>
<br />
<input type="button" value="複製" id="copy" />
</body>
</html>
在非 IE 的瀏覽器上, jQuery 的 clone 方法可以正確的把已勾選的 checkbox 的 checked 狀態複製下來,但在 IE 上卻不行,如下圖:

所以這裡只好針對 IE 再進行一次特別的處理:
$(function () {
$('#copy').click(function () {
var $checkedValues = $('input.test:checked').clone();
if ($.browser.msie) {
$checkedValues.attr('checked', true);
}
$('#target').html('').append($checkedValues);
});
});
原理很簡單,就是遇到 IE 時,再將複製下來的 checkbox 的 checked 屬性設為 true 即可。
補充:IE6 的逆襲
不過上述程式在 IE6 又會出現問題了,在 IE6 上執行時,會發現 checked 屬性無法正常被設為 true 。
雖然沒有深入去研究,但我猜想是 IE6 在處理 DOM 時的問題,因此我祭出了 setTimeout 大法:
$(function () {
$('#copy').click(function () {
var $checkedValues = $('input.test:checked').clone();
if ($.browser.msie) {
setTimeout(function () { $checkedValues.attr('checked', true); }, 0);
}
$('#target').html('').append($checkedValues);
setTimeout(function () {
// 其他可能會對處理到 checkbox 的動作
}, 0);
});
});
這裡 setTimeout 可以確保程式在 DOM 完全更新後,再執行下一步的動作。
2008年08月2日
[JavaScript] 五分鐘小教室 - 不重複送出 Ajax Request
這次在設計購物車時,遇到了以下的介面:

客戶的需求是在按下「 + 」 或「 - 」時,要以 Ajax 發送更新的數量到後端系統去驗算;每按一次「 + 」 或「 - 」,就要送出一次 Ajax Request。
可是這時候問題就來啦,如果數量要 10 個的話就要連續按 10 次「 + 」,也會連續發送 10 次的 Ajax Request ;這樣不但會浪費珍貴的網路頻寬,更不用說會造成後端系統的負擔。
怎麼解決呢?其實方法很多,而這裡我採用最簡單的 setTimeout 和 clearTimeout 。程式如下:
var sending = null;
var _formSubmit = function () {
alert('Form submited!');
};
var _doAjaxPost = function () {
if (sending !== null) {
clearTimeout(sending);
sending = null;
}
sending = setTimeout(_formSubmit, 1000);
};
var plusQuantity = function () {
// ... 執行增加數量的動作 ...
_doAjaxPost();
return false;
};
var minusQuantity = function () {
// ... 執行減少數量的動作 ...
_doAjaxPost();
return false;
};
$(function () {
// 增加數量
$('a.plus').click(plusQuantity);
// 減少數量
$('a.minus').click(minusQuantity);
});
註:這裡我大量使用了 jQuery 的功能。
想法很簡單,就是當我們按下「 + 」 或「 - 」時,要隔一秒才會送出 Ajax Request ;而在這一秒內如果再次按下「 + 」 或「 - 」,那麼就重新計時。
因此程式的主要重點在 _doAjaxPost 這個函式以及全域變數: sending ;當第一次呼叫 _doAjaxPost 時 sending 還是 null ,這時我們利用 setTimeout 開始計時,並將計時器指定給 sending 這個變數。而當第二次呼叫時, sending 變數已經不為空值,因此我們再利用 clearTimeout 將它清除,並重設為 null 以達到重新計時的目的。
是不是很簡單呢?
如果各位有更好的作法,也歡迎分享~
2008年05月16日
[jQuery] 自製 jQuery Plugin - Part 2
在 Part 1 我們看到如何建立一個 jQuery Plugin 的雛形,也讓它能夠做一些簡單的動作了。只是這個 Plugin 似乎沒什麼太大的用途,所以接下來我們就來寫個真正能用的東西。
簡單的頁籤功能
要舉個簡單又實用的教學用例子,一直都是非常困難的事情。經過多次的思考與挑選,我決定用頁籤功能 (Tab) 來當做本文練習的重點。
當然 jQuery 已經有一些 Tab 相關的 Plugin 了,而且基於不重造輪子的理念下,我們似乎不應該自己動手;不過站在學習的角度下,如果我們自己實作一個簡單的頁籤的話,將會有助於瞭解 jQuery Plugin 的設計過程。
以下先來瞭解我們需要的功能,再來想想看怎麼實作它。
首先我們的頁籤大概會長成這樣子:



基本上我們要的效果就是在點選上面的 TAB1 、 TAB2 及 TAB3 時,底下的文字區塊會跟著切換,這就是最簡單的頁籤功能了。
頁面結構
在 HTML 的部份也非常簡單,基本上是一個 UL 清單 (div.tabs ul) 加上三個 DIV 區塊 (div.tabBlock) ,最後再用一個大 DIV 區塊 (div#mytab) 把它們包起來。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>jQuery TEST</title>
<link href="mytab.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="mytab">
<div class="tabs">
<ul>
<li class="active"><a href="#tab1"><span>TAB1</span></a></li>
<li><a href="#tab2"><span>TAB2</span></a></li>
<li><a href="#tab3"><span>TAB3</span></a></li>
</ul>
</div>
<div id="tab1" class="tabBlock">
有時候寫 jQuery 時,常會發現一些簡單的效果可以重複利用。只是每次用 Copy & Paste 大法似乎不是件好事,有沒有什麼方法可以讓我們把這些效果用到其他地方呢?
</div>
<div id="tab2" class="tabBlock">
沒錯,就是用 jQuery 的 Plugin 機制。不過 jQuery 的 Plugin 機制好像很難懂?其實一點也不。以下我用最簡單的方式來為大家解說如何自製一個簡單的 Plugin 。
</div>
<div id="tab3" class="tabBlock">
當然在此之前,你得先瞭解 JavaScript 的 class 、 object 、 variables scope 還有 anonymous function 等基礎...
</div>
</div>
<script type="text/javascript" src="jquery/1.2.3.js"></script>
<script type="text/javascript" src="jquery/mytab.js"></script>
<script type="text/javascript">
$(function () {
$('#mytab').mytab();
});
</script>
</body>
</html>
當然別忘了把 JavaScript 檔案引入,這裡我先把 mytoolbox.js 改為 mytab.js ,因為我們要做的是頁籤。
而頁籤樣式的部份則以是 CSS 來完成,請把它存檔並命名為 mytab.css :
div.tabBlock {
clear:both;
margin-top:-1px;
border:1px solid #CCC;
padding:5px;
}
div.tabs {
margin-bottom:-1px;
overflow:hidden;
}
div.tabs ul {
margin:0;
padding:0;
list-style:none;
}
div.tabs ul li {
float:left;
height:25px;
margin:0 3px;
line-height:25px;
font-size:9pt;
border:1px solid #CCC;
overflow:hidden;
}
div.tabs ul li a {
display:block;
padding:3px;
color:#000;
text-decoration:none;
}
div.tabs ul li.active a,
div.tabs ul li a:hover {
color:#FFF;
background:#999;
}
這裡我們就不深究 CSS 怎麼做的了,有興趣的朋友請自行參考相關書籍。
準備 Plugin 樣版
現在把 Part 1 的 mytoolbox.js 複製為 mytab.js ,然後稍做修改,如下:
;(function($) {
$.fn.mytab = function(settings) {
var _defaultSettings = {};
var _settings = $.extend(_defaultSettings, settings);
var _handler = function() {
// 從這裡開始
};
return this.each(_handler);
};
})(jQuery);
從上面的程式中可以看到我把 _defaultSettings 的內容清掉了,因為我們暫時用不到特別的設定。然後我把給 each 方法用的 callback 獨立為 _handler 函式,因為後面我們要做的動作大部份都會在這裡發生。
呈現頁籤下的區塊
在套上 CSS 後,我們會看到三個 div.tabBlock 區塊同時都顯示出來了,這樣做的目的其實是為了當瀏覽器沒有開 JavaScript 時還能呈現資訊。而在開啟了 JavaScript 後,我希望只呈現第一個 div.tabBlock 區塊,這時我們就可以利用 jQuery 來完成:
var _handler = function() {
$('div.tabBlock', this).hide().eq(0).show();
};
在解釋原理之前,先回頭看一下 HTML 頁面呼叫 Plugin 的地方,你會發現我把 Plugin 套用在 div#mytab 這個元素上,所以在 _handler 裡的 this 其實是指向 div#mytab 。
瞭解 this 代表的意義後,接著回到 _handler 中,我們就可以知道第一行的 $('div.tabBlock', this) 其實就是指抓取 div#mytab 底下的三個 div.tabBlock 元素。所以第一行我們先把所有的 div.tabBlock 隱藏起來,然後利用 .eq(0).show() 把第一個 div.tabBlock 顯示出來。
讓頁籤動起來
接著我們先讓頁籤在點選時能夠切換它的反白狀態,而反白的效果我們已經定義 CSS 裡了,也就是讓 LI 的 class 變成 active 即可:
var _handler = function() {
$('div.tabBlock', this).hide().eq(0).show();
$('div.tabs li a', this).click(function () {
$('div.tabs li').removeClass('active');
$(this).parent('li').toggleClass('active');
return false;
});
};
這邊的原理也很簡單,首先先讓所有 div.tabs li 都移除反白效果,然後再對我們按的連結的上一層 LI 元素套上 active 。注意這裡也有個 this ,它代表的是目前點選的頁籤連結。
最後我們要 return false ,以阻止 click 事件被往上傳遞。
現在點選看看頁籤連結,是不是能反白了呢?
註:滑過頁籤會呈現反白這個效果也是定義在 CSS 裡,試著找找看吧。
記住「這個」
在繼續完成效果前,有個問題我得先說明一下。雖然我們在測試頁面上只佈置了一個 Tab 元件,不過很難保證頁面上不會有其他地方也會用到頁籤效果,這時我們的 Plugin 可能會產生副作用。
問題出在 $('div.tabs li').removeClass('active') 這行,因為我們不能確定頁面其他地方是不是也有有元素符合 $('div.tabs li') 。所以我在這裡應該要明確指定這些 LI 元素應該屬於那個元素,也就是 $('div.tabs li a', this) 的 this (即 div#mytab ) 。
不過在 click 方法指定的匿名函式中, this 又指到 a 元素,而不是我們要的 div#mytab ;那麼有什麼方法能在 click 中找到 div#mytab 呢?總不能把 div#mytab 寫死在 Selector 中吧?其實方法很簡單,就是在 click 外先定義一個新變數指向外部的 this :
var _handler = function() {
var container = this; // 加入這行,並將以下表示 div#mytab 的 this 改為 container
$('div.tabBlock', container).hide().eq(0).show();
$('div.tabs li a', container).click(function () {
$('div.tabs li', container).removeClass('active');
$(this).parent('li').toggleClass('active'); // 這個 this 不用動,它表示 a 元素
return false;
});
};
因為在 click 的匿名函式會繼承外部的作用域,使得 container 的 scope 得以存在於 click 的 callback 裡;因此我們就能放心的使用 container 來表示 div#mytab 了。
註:在 click 裡的 callback 又有另一個名稱: closure ,因為它用到了外部 function 所定義的變數。
切換對應的區塊
接著繼續完成我們要的功能,也就是切換頁籤對應的區塊。
這裡我用了連結錨點的技巧,這個技巧本身有個優點:就是當 JavaScript 被禁用時,錨點還能正常動作。而錨點的名稱剛好就是頁籤所對應的內容區塊 ID ,這就方便我們找到要顯示的內容區塊。
程式碼如下:
var _handler = function() {
var container = this;
$('div.tabBlock', container).hide().eq(0).show();
$('div.tabs li a', container).click(function () {
$('div.tabs li', container).removeClass('active');
$(this).parent('li').toggleClass('active');
$('div.tabBlock', container).hide(); // 先全部藏起來
var id = (String(this.href).match(/(#.+)$/))[1]; // 只抓對應的 tabBlock id
$(id).show(); // 顯示對應的 tabBlock
return false;
});
};
原理一樣很簡單,當按下頁籤連結時,先隱藏所有內容區塊 (div.tabBlock) ,然後取得連結的 href 位址中的錨點名稱,以顯示頁籤所對應的內容區塊。不過這裡要注意一點,那就是瀏覽器通常會幫我們在錨點名稱前加入目前完整的網址;因此我這裡便使用正規式來取得帶井字號的錨點名稱,也剛好直接讓 jQuery 使用。
減少多餘的查詢
再看一次程式,我發現有個地方重複查詢了兩次,那就是 $('div.tabBlock', container) ;第一次是在我們只顯示第一個內容區塊時,而第二次則在點選連結時要隱藏所有內容區塊時。這裡我們可以利用暫存變數來解決:
var _handler = function() {
var container = this;
var $tabBlocks = $('div.tabBlock', container); // 加入這行
$tabBlocks.hide().eq(0).show(); // 改用 $tabBlocks
$('div.tabs li a', container).click(function () {
$('div.tabs li', container).removeClass('active');
$(this).parent('li').toggleClass('active');
$tabBlocks.hide(); // 改用 $tabBlocks
var id = (String(this.href).match(/(#.+)$/))[1];
$(id).show();
return false;
});
};
不過也不是每個重複的查詢都要用暫存變數,因為假設當你在 Plugin 運作的過程中,對 div.tabBlock 有進行增減的話,那麼重複再查一次就是必要的動作了,這樣才能確保我們抓到正確數量的元素集。
到這裡我們的 mytab.js 初版算是完成了,試試看它是不是依照我們的要求正確動作呢?
加強 Plugin
雖然我們的 Plugin 已經可以動作了,但其實還是有些地方可以加強;而加入這些功能其實就是為了讓 Plugin 能更有彈性,以應付各種不同的狀況。
這裡我簡單介紹兩個加強的功能:
由外部決定 class
我們希望可以指定 active 的名稱,讓外部可以自行決定。我們在 _defaultSettings 中多定義了一個 activeClass 項目,然後把程式裡的所有 'active' 改為 _settings.activeClass 。程式碼修改如下:
$.fn.mytab = function(settings) {
var _defaultSettings = {
activeClass: 'active'
};
var _settings = $.extend(_defaultSettings, settings);
var _handler = function() {
var container = this;
var $tabBlocks = $('div.tabBlock', container);
$tabBlocks.hide().eq(0).show();
$tabLinks.click(function () {
$tabLists.removeClass(_settings.activeClass);
$(this).parent('li').toggleClass(_settings.activeClass);
$tabBlocks.hide();
var id = (String(this.href).match(/(#.+)$/))[1];
$(id).show();
return false;
});
};
return this.each(_handler);
};
同樣的原理,我們可以更改 div.tabs 和 div.tabBlock 的 class 名稱,這邊我留給各位自行試試。
自動切換內容區塊
現在我希望進入頁面時,能讓內容區塊自動跳到我指定的頁籤。這裡我有兩種方式可以指定:由網址決定以及用 Server 程式決定。
以網址決定就是我們在瀏覽器的網址列的網址後面再加上錨點名稱,例如:
http://localhost/mytab.htm#tab2
這個網址可以由外部的網頁以連結的方式指定,這樣的話進入我們這個頁籤畫面時,我們就可以依照這個錨點來決定要顯示的內容區塊。程式很簡單:
var _handler = function() {
var container = this;
var $tabBlocks = $('div.tabBlock', container);
// 加入以下這段
var matches = (String(location.href).match(/(#.+)$/));
if (null !== matches) { // 有找到網址錨點的話就切換內容
var id = matches[1];
$tabBlocks.hide(); // 先把全部的內容區塊藏起來
$(id).show(); // 顯示錨點對應的內容區塊
// 將對應的頁籤連結反白
$('div.tabs li', container).removeClass(_settings.activeClass);
$('div.tabs li a', container).each(function () {
if (-1 !== String(this.href).indexOf(id)) {
$(this).parent('li').toggleClass(_settings.activeClass);
}
});
} else {
$tabBlocks.hide().eq(0).show();
}
// ... 略 ...
};
原理就是透過錨點來顯示對應的內容區塊,再跟著把對應頁籤連結反白即可;如果沒有指定錨點的話,就顯示第一個內容區塊。
當然別忘了重構,把多餘的查詢動作用暫存變數取代:
var _handler = function() {
var container = this;
var $tabBlocks = $('div.tabBlock', container);
var $tabLists = $('div.tabs li', container);
var $tabLinks = $('div.tabs li a', container);
var matches = (String(location.href).match(/(#.+)$/));
if (null !== matches) {
var id = matches[1];
$tabBlocks.hide();
$(id).show();
$tabLists.removeClass(_settings.activeClass);
$tabLinks.each(function () {
if (-1 !== String(this.href).indexOf(id)) {
$(this).parent('li').toggleClass(_settings.activeClass);
}
});
} else {
$tabBlocks.hide().eq(0).show();
}
$tabLinks.click(function () {
$tabLists.removeClass(_settings.activeClass);
$(this).parent('li').toggleClass(_settings.activeClass);
$tabBlocks.hide();
var id = (String(this.href).match(/(#.+)$/))[1];
$(id).show();
return false;
});
};
回到主題,我們的另外一種指定內容區塊的方式就是在 Server 端輸出 HTML 時,就先決定好 active 的 LI 元素了。假設現在頁籤部份的 HTML 如下:
<div class="tabs">
<ul>
<li><a href="#tab1"><span>TAB1</span></a></li>
<li class="active"><a href="#tab2"><span>TAB2</span></a></li>
<li><a href="#tab3"><span>TAB3</span></a></li>
</ul>
</div>
現在 TAB2 這個 LI 元素的 class 是 active ,那麼我們應該怎麼自動切換到對應的內容區塊呢?很簡單,就讓 li.active 底下的 a 「點下去」就可以了。程式如下:
var _handler = function() {
// ... 略 ...
$tabLinks.click(function () {
$tabLists.removeClass(_settings.activeClass);
$(this).parent('li').toggleClass(_settings.activeClass);
$tabBlocks.hide();
var id = (String(this.href).match(/(#.+)$/))[1];
$(id).show();
return false;
});
// 加入這段
var $activeLink = $('div.tabs li.' + _settings.activeClass + ' > a', container);
if (0 !== $activeLink.size()) {
$activeLink.trigger('click');
}
};
原理就是在我們指定好頁籤連結的 click 事件後,再觸發 li.active 底下的連結的 click 事件即可,很簡單吧。
測試再測試
以上我們都只在單一個頁籤元件上測試而已 (也就是 div#mytab ) ,但一般來說大部份的 Plugin 應該要能在頁面中被套用到多個元件上;換句話說就是頁面上會好幾個有頁籤的區塊,所以這裡我們得測試這個可能發生的狀況。
我在原來的 div#mytab 底下再加入一個 div#mytab2 ,內容和 div#mytab 差不多,只是把 #tab1, #tab2, #tab3 改成 #tab4, #tab5, #tab6 而已:
<div id="mytab2">
<div class="tabs">
<ul>
<li><a href="#tab4"><span>TAB4</span></a></li>
<li class="active"><a href="#tab5"><span>TAB5</span></a></li>
<li><a href="#tab6"><span>TAB6</span></a></li>
</ul>
</div>
<div id="tab4" class="tabBlock">
aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa
</div>
<div id="tab5" class="tabBlock">
bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb
</div>
<div id="tab6" class="tabBlock">
ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc
</div>
</div>
然後我也為 div#mytab 和 div#mytab2 加入一組 class="mytab" :
<div id="mytab" class="mytab">
... 略 ...
<div id="mytab2" class="mytab">
現在我們把 HTML 頁面上套用 Plugin 的部份稍作修改,也就是把 id 換成 class :
$(function () {
$('.mytab').mytab();
});
然後我們重新瀏覽一下,一切看起來很正常。直到我測試了用網址錨點來自動切換內容區塊時...出現了下圖的奇怪現象:

原因出在我們發現錨點時,就會先隱藏所有內容區塊,然後才顯示錨點對應的內容區塊;但是這樣就會使得沒有錨點對應的內容區塊通通不顯示,出現了上圖的狀況。
怎麼辦?其實也很簡單,就是將該頁籤元件所擁有的頁籤連結對應的錨點先記下來,然後找找看網址描點有沒有在這裡面,有的話才做上面的動作,不然就一定要顯示第一個內容區塊:
var _handler = function() {
var container = this;
var $tabBlocks = $('div.tabBlock', container);
var $tabLists = $('div.tabs li', container);
var $tabLinks = $('div.tabs li a', container);
var tabIdList = [];
// 先記住所有頁籤連結對應的錨點
$tabLinks.each(function () {
var matches = (String(this.href).match(/(#.+)$/));
if (null !== matches) {
tabIdList.push(matches[1]);
}
});
var matches = (String(location.href).match(/(#.+)$/));
if (null !== matches // 錨點在列表裡的話就顯示
&& -1 !== $.inArray(matches[1], tabIdList)) {
var id = matches[1];
$tabBlocks.hide();
$(id).show();
$tabLists.removeClass(_settings.activeClass);
$tabLinks.each(function () {
if (-1 !== String(this.href).indexOf(id)) {
$(this).parent('li').toggleClass(_settings.activeClass);
}
});
} else {
$tabBlocks.hide().eq(0).show();
}
// ... 略 ...
};
這裡我用了最簡單 JavaScritp 的陣列,還有 jQuery 的 $.inArray 方法來解決這個問題;想想看,有沒有更方便的解法呢?
之後當然我們還得再多試試幾個不同的狀況,看看還有沒有需要解決的部份,這裡就留給大家試試看囉。
還有什麼
在看完以上的介紹後,我想自己寫一個 jQuery 的 Plugin 其實並不困難,困難的是我們要怎麼去完成裡面的內容。所以像是對 JavaScript 的作用域的認知、各家瀏覽器的差異、 DHTML 的基本功,還有如何去呈現效果的想像力等,這些都是在開發 jQuery Plugin 非常重要的。
另外就是一定要對程式碼做重構與測試的動作,因為它們會影響這個 Plugin 的效能與穩定性。這裡可以多參考其他 Plugin 作者的程式碼,觀察他們是如何處理效能問題;然後最好能建立一個測試頁面,把有可能遇到的使用方式儘可能地包含進來,以便測試 Plugin 的正確性。
希望透過這兩篇簡單的教學,能讓大家能快速進入 jQuery Plugin 的世界;也希望大家如果開發了好的 Plugin 後,能不吝分享出來。
感謝大家~~謝謝收看。
2008年05月13日
[jQuery] 自製 jQuery Plugin - Part 1
有時候寫 jQuery 時,常會發現一些簡單的效果可以重複利用。只是每次用 Copy & Paste 大法似乎不是件好事,有沒有什麼方法可以讓我們把這些效果用到其他地方呢?
沒錯,就是用 jQuery 的 Plugin 機制。
不過 jQuery 的 Plugin 機制好像很難懂?其實一點也不。以下我用最簡單的方式來為大家解說如何自製一個簡單的 Plugin 。
當然在此之前,你得先瞭解 JavaScript 的 class 、 object 、 variables scope 還有 anonymous function 等基礎,這些可以參考「 JavaScript 大全」一書。
Plugin 樣版
寫 jQuery 的 Plugin 最快的方法就是拿現成的 Plugin 來改,只是在那麼多的 Plugin 中怎麼找到好的範例呢?別擔心,這邊我提供一個最簡單的範例樣版:
jQuery.fn.mytoolbox = function() {
return this.each(function() {
});
};
首先, mytoolbox 就是我們的 plugin 名稱,利用 jQuery.fn 我們可以將它註冊為 jQuery 的 plugin 。然後我們把 jQuery.fn.mytoolbox 指向一個匿名函式 (anonymous function) ,又稱為 callback ;而這個 callback 的內容很簡單,就是利用 jQuery 的 each 方法,來一一執行對應的動作。
特別要注意匿名函式裡的 this 關鍵字,它會指向一個 jQuery 物件;而這個 jQuery 物件則是我們要指定的,稍後我會再進一步說明。
使用 Plugin
現在將上面的樣版存成 mytoolbox.js ,和 jquery.js 放在一起。然後建立一個 HTML 測試檔案,內容如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>jQuery TEST</title>
<style type="text/css">
.test {
border:1px solid #CCC;
cursor:pointer;
padding:3px;
}
</style>
</head>
<body>
<div id="test1" class="test">
點我!
</div>
<div id="test2" class="test">
點我!
</div>
<div id="debug"></div>
<script type="text/javascript" src="jquery/1.2.3.js"></script>
<script type="text/javascript" src="jquery/mytoolbox.js"></script>
<script type="text/javascript">
$(function () {
$('.test').mytoolbox();
});
</script>
</body>
</html>
首先 HTML 中引用了 jQuery 函式庫及我們寫的 Plugin 檔案,然後我在畫面上佈置了兩個 class 為 test 的 div 元素。接著我們用以下程式碼來呼叫我們的 Plugin :
$(function () {
$('.test').mytoolbox();
});
這邊的用意就是將上面那兩個 div 套上 mytoolbox 這個 Plugin ,這樣 Plugin 就能動了,很簡單吧?
加入動作
當然,這個 Plugin 什麼事都還沒開始做,是個空骨架而已。現在我們要為它加血添肉,讓它動起來。
先簡單在 each 的 callback 裡加入一行:
jQuery.fn.mytoolbox = function() {
return this.each(function() {
alert(this.id); // 加入此行
});
};
再重新瀏覽測試用的 HTML 檔,你會發現頁面自動跳出了兩次訊息視窗,內容分別是 test1 和 test2 ;這證明了我們的 Plugin 的確有套用在 class 為 test 的兩個 div 上面。
不過現在有兩個 this ,它們是一樣的東西嗎?不,因為 scope 及觸發對象的不同,它們兩個是不同的東西。在外面的 this 是一個 jQuery 物件,指向我們指定的 $('.test') 這個物件;而 each callback 裡的 this 則是 div 元素,因為 each 是個 iterator function ,因此 alert(this.id) 會執行兩次。在第一次的 this 會指向 #test1 這個 div ,第二次則指向 #test2 這個 div 。
註:這裡我用 #test1 表示 id 為 test1 的元素。
現在我希望改成按下 div 元素後才會 alert 該元素的 id ,這要怎麼做呢?我們要改用 click 事件,做法如下:
jQuery.fn.mytoolbox = function() {
return this.each(function() {
jQuery(this).click(function () {
alert(this.id);
});
});
};
由於 each callback 裡的 this 是 DOM 元素,所以我們要用 jQuery() 把 this 包起來,這樣才能方便指定該元素的 click 事件。現在重新瀏覽頁面,點選任何一個 div ,應該就會跳出對應的訊息視窗了。
再包一層
如果在 each 的 callback 裡會呼叫到多次的 jQuery 的話,一直寫 jQuery 這幾個字實在是很累人的一件事;而且 jQuery 不是可以簡寫成 $ 號嗎?不能直接用嗎?當然可以,只是這樣可能會和其他 JavaScript Library 發生衝突;所以我們要改用以下的方式來包覆我們的 Plugin :
;(function($) {
$.fn.mytoolbox = function() {
return this.each(function() {
$(this).click(function () {
alert(this.id);
});
});
};
})(jQuery);
JavaScript 可以直接用一組小括號 [()] 包覆一個匿名函式,然後後面再接一組小括號 [()] 表示呼叫這個匿名函式;而第二組小括號中就可以放置這個匿名函式的參數。所以在上面的程式碼中,我們把 Plugin 的程式碼用一個匿名函式包覆起來,然後參數就用我們常用的 $ 符號;接著在利用前述的原理,將 jQuery 這個類別導入給我們的 Plugin ,這樣我們就可以很快樂地在 Plugin 中使用我們熟悉的 $ 符號了。至於最前面的分號 (;) ,主要是考慮這個 Plugin 檔案會和其他 JS 檔合併壓縮而放進來的。
註: $ 在 JavaScript 裡是合法的變數名稱。
後面的說明我會略過這個包覆動作,在實際檔案中請別忘了加。
加入選項設定
接下來我希望讓 each 的 callback 函式能讓使用者自訂,因此我需要一個讓使用者能設定的選項。就像其他的 Plugin 一樣,我們讓我們的 mytoolbox 可以接受一個 JSON 物件:
$.fn.mytoolbox = function(settings) {
var _defaultSettings = {
callback: function () {
alert(this.id);
}
};
var _settings = $.extend(_defaultSettings, settings);
return this.each(function() {
$(this).click(_settings.callback);
});
};
首先我們為 Plugin 加入 settings 參數,也就是一般 Plugin 常見的設定值。然後則是 _defaultSettings ,它能幫我們在使用者沒有指定任何設定值給 settings 時,還能夠提供預設的設定值。
接著我用 jQuery 提供的 extend 方法,將 settings 中有設定的值覆蓋掉 _defaultSettings 所設定的預設值,再把結果存放在 _settings 這個變數中;後面我們就會用新的 _settings 變數當做我們的設定值。
現在我們在 _settings 中指定了一個 callback 項目 (預設是用 alert ) ,然後將它指定給 div 元素的 click 觸發器。現在我要在 HTML 頁面中更改這個事件處理器,使它不再使用 alert ,而是把結果顯示在 div#debug 裡。程式如下:
$(function () {
var debug = $('#debug');
$('.test').mytoolbox({
callback: function () {
debug.html(debug.html() + this.id + '<br />');
}
});
});
再重新瀏覽一次頁面,看看效果是不是依照我們想像的完成呢?
修改觸發事件
假設現在我們不想用 click ,而是想讓滑鼠移過就觸發 callback 呢?這時就要借重 jQuery 的 bind 方法了:
$.fn.mytoolbox = function(settings) {
var _defaultSettings = {
bind: 'click',
callback: function () {
alert(this.id);
}
};
var _settings = $.extend(_defaultSettings, settings);
return this.each(function() {
$(this).bind(_settings.bind, _settings.callback);
});
};
這裡我加入一個 bind 設定項目,預設是用 click 事件觸發。回到 HTML 頁面,我們改用 mouseover 來觸發 callback :
$(function () {
var debug = $('#debug');
$('.test').mytoolbox({
bind: 'mouseover',
callback: function () {
debug.html(debug.html() + this.id + '<br />');
}
});
});
重新瀏覽 HTML 頁面,當滑鼠移過 div 元素時,是不是會出現對應的 id 呢?
到這裡,相信大家都應該大致瞭解如何建立一個 jQuery Plugin 了吧?接下來,我將透過實際的例子為大家介紹更多自製 jQuery Plugin 所需要注意的地方。
請觀賞 Part 2 。
參考網址
2008年01月7日
dean 的 IE7 Library 釋出新版
完整文章連結:IE7.js version 2.0 (beta)
這次從 0.9 直接跳到 2.0 beta ,而且拆成了 IE7.js 和 IE8.js 兩個部份。
IE7.js 把功能只精簡到微軟正牌 IE7 有實作的部份,其他加強的部份就移到 IE8.js 上了 (因為 IE8 已經趕上 Web 標準了) 。其他修正部份如下:
- The IE7 project is now hosted on googlecode (I got fed up with SourceForge).
- IE7 is no longer modular. Instead I’ve merged the scripts into two: IE7.js and IE8.js
- IE7.js includes only fixes that are included in the real MSIE7 browser.
- All other enhancements are moved to IE8.js.
- IE7 is now much smaller (11KB gzipped).
- IE7 is now much faster (it uses the selector engine from base2.DOM)
- There are no dependencies on other files (except blank.gif)
- You can hotlink IE7/IE8.js directly from Google’s servers (usage instructions below)
- The fix for base64 encoded images is no longer included
看起來不錯喔,有興趣的朋友不妨試一下。
2007年09月28日
[JavaScript] window.onresize 連續觸發的問題
今天遇到了一個怪異的問題,就是在 IE6 底下 window 的 onresize 這個事件竟然會在視窗大小沒有改變的情況下被連續觸發;試了一下 Firefox 和 IE7 都沒有這個問題,所以我便懷疑是 IE6 有實作上的錯誤。
為了查證,我 Google 了一下,找到了以下這篇文章:
IE Fires Onresize When Body Resizes
原來 IE6 會在 body 被 resize 時,同時觸發 window 的 resize 事件。
不過 body 什麼時候會被 resize 呢?通常就是利用 display 來顯示或隱藏元素的時候,例如:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type"
content="text/html; charset=utf-8" />
<title>window.onresize</title>
<script type="text/javascript">
var link = null;
var block = null;
var resizeWindow = function () {
alert('resized');
};
window.onload = function () {
link = document.getElementById('link');
block = document.getElementById('block');
link.onmouseover = function () {
block.style.display = 'block'
};
link.onmouseout = function () {
block.style.display = 'none'
};
window.onresize = resizeWindow;
};
</script>
</head>
<body>
<a href="window.onresize.html" id="link">Link</a>
<div id="block" style="display:none;">
<p>Show something.</p>
</div>
</body>
</html>
這時候當滑鼠移到 Link 上時,因為元素會有消長,所以 body 的 resize 就會被觸發 (也即 reflow 這個觀念) ,只是這時候 IE6 就會很雞婆地一起觸發 window 的 resize 事件,而且會一直連續觸發;如果這時我們在 window.onresize 有設定要處理某些東西的話 (例如上面的 resizeWindow 函式) ,就會使得真正的 mouseover 和 mouseout 事件無法正常動作。
而我會遇到這個狀況是因為我需要在 window.onresize 裡處理一些排版上的問題,也就是在視窗大小變化後,有些透過 JavaScript 來調整寬度的選單就需要重新調整。但是這時如果連續觸發了 window 的 resize 事件,就會造成整個程式執行起來頓頓的。
還好上面找到的文章提供了一個解法,那就是判斷 document.documentElement.clientHeight 是不是有被改變了。所以我只要把 resizeWindow 這個事件改成以下的樣子就可以了:
var currentClientHeight = 0;
var resizeWindow = function () {
if (currentClientHeight != document.documentElement.clientHeight) {
alert('resized');
}
currentClientHeight = document.documentElement.clientHeight;
};
雖然 window 的 resize 事件還是會被連續觸發,但是至少觸發時所需要執行的動作減少了,這樣就不會讓程式執行起來感覺頓頓的了。
2007年09月17日
[JavaScript] jQuery UI 1.0 釋出
隨著 jQuery 1.2.1 的推出,一個 Interface 的後繼者也出現了,它叫做 jQuery UI 。
jQuery UI: Interactions and Widgets
小試了一下它的 DEMO ,還是有一些 Bug ,不過整體來說還是非常不錯。
主要有以下的功能:
- Draggables
- Droppables (有 Bug)
- Sortables
- Selectables
- Resizables
- Accordion
- Calendar (有 Bug)
- Dialog
- Slider
- Tablesorter
- Tabs
- Magnifier
- Shadow
