テーブル行を選択するスクリプト

/web/javascript

Note: この記事は、3年以上前に書かれています。Webの進化は速い!情報の正確性は自己責任で判断してください。

1 rowSelect rowSelect rowSelect rowSelect rowSelect
2 rowSelect rowSelect rowSelect rowSelect rowSelect
3 rowSelect rowSelect rowSelect rowSelect rowSelect
4 rowSelect rowSelect rowSelect rowSelect rowSelect
5

rowSelect

rowSelect

rowSelect

rowSelect

rowSelect

6 rowSelect rowSelect rowSelect rowSelect rowSelect

ライブラリを使わないtable要素の行選択。とゆーか<tr>要素のclassを動的に変更するスクリプト。YUIでいうこの辺

今回のサンプルはより複雑な動作を実現する踏み台という位置づけであります。選択した行の<tr>要素を取得するという動作が必要。オマケでclassの変更。見た目だけならCSSのhoverでも使えば良いし、ライブラリもいっぱいある。Ext.jsとか。自分で組んだほうがカスタム楽そうだ、ということ。

classの取得/設定、Eventオブジェクトとthisの扱いには苦労した。IE逝ってよし。今日ほどサイ本をありがたく思った日はありません。みんなサイ本買おう。

以下、細かい解説。(デモは「続き」で動きます)

行選択: rowSelect()

  1. var rowSelect = {
  2. init: function(table){
  3. this.rowList = $(table).getElementsByTagName('tr');
  4. for(i=0; i<this.rowList.length; i++){
  5. addEvent(this.rowList[i],'mouseover',rowSelect.msOver,false);
  6. addEvent(this.rowList[i],'mouseout',rowSelect.msOut,false);
  7. addEvent(this.rowList[i],'click',rowSelect.selected,false);
  8. }
  9. },
  10.  
  11. msOver: function(e){
  12. var row = e.currentTarget;
  13. if(row.className == 'msOver') return;
  14. if(this.selectFlag) return;
  15. addClass(row,'msOver');
  16. },
  17.  
  18. msOut: function(e){
  19. var row = e.currentTarget;
  20. if(row.className != 'msOver') return;
  21. if(this.selectFlag) return;
  22. delClass(row,"msOver");
  23. },
  24.  
  25. selected: function(e){
  26. var row = e.currentTarget;
  27. if(this.selectFlag){
  28. delClass(row,"msOver");
  29. delClass(row,"selected");
  30. this.selectFlag = null;
  31. return;
  32. }
  33. this.selectFlag = true;
  34. delClass(row,"msOver");
  35. addClass(row,"selected");
  36. }
  37. }

スクリプト本体はこんな感じになってます。mouseoverでハイライト。mouseoutで解除。行をクリックして選択。選択行にonmouseしても変化なし。選択行を再クリックすれば選択解除。ここまで。

後は汎用として個別に組んだ、getElementByIdでオブジェクトを返す$()関数、イベント登録用のaddEvent()、classを確認/登録/削除するhasClass()/addClass()/delClass()辺りを適宜呼び出して使ってます。

attachEvent()とthisの関係

今回の目的は行選択での<tr>要素の取得です。イベントは各<tr>要素に対して設定しているため、たとえばrowSelect.msOver()内ではthisに呼び出し元(イベントが登録された要素)の<tr>要素が入ってます。これを使えばOK。

  1. var rowSelect = {
  2. init: function(table){
  3. ... goodness ...
  4. this.rowList[i].onmouseover = rowSelect.msOver;
  5. ... goodness ...
  6. },
  7.  
  8. msOver: function(){
  9. var row = this; // rowには<tr>要素が入る
  10. ... goodness ...
  11. },

ただしaddEventListener()/attachEvent()を用いてイベントハンドラを登録する場合、IEでは取得できませんでした。何故か? グローバル変数として呼び出されているからです。なのでIEだと、thisにはwindowオブジェクトが入ってます。

  1. var rowSelect = {
  2. init: function(table){
  3. ... goodness ...
  4. this.rowList[i].attachEvent('onmouseover', rowSelect.msOver);
  5. ... goodness ...
  6. },
  7.  
  8. msOver: function(){
  9. var row = this; // rowにはwindowオブジェクトが入る
  10. ... goodness ...
  11. },

DOM2対応のブラウザなら、thisが使えずとも、EventオブジェクトのcurrentTargetプロパティにそのイベントが登録された要素が入っている。でもIEのEventオブジェクトは独特なので、currentTargetに相当するものがない。targetプロパティに相当するsrcElementプロパティならある...が、これには「イベントが発生した要素」が入っており、この場合だと<tr>ではなく、その中身の<td>だったり<p>だったり<img>だったりする。なので、サイ本17.3.8辺りを参考にイベントハンドラ登録用の関数を以下のように改造してみた。

  1. var addEvent = function(elm, evType, fn, useCapture) {
  2. if (elm.addEventListener) {
  3. elm.addEventListener(evType, fn, useCapture);
  4. return true;
  5. }
  6. else if (elm.attachEvent) {
  7. // add
  8. var IEfn = function(e){
  9. if(!e) e = window.event;
  10. e.currentTarget = elm;
  11. if(Function.prototype.call)
  12. fn.call(elm,e)
  13. else{
  14. elm._currentHandler = fn;
  15. elm._currentHandler(e);
  16. elm._currentHandler = null;
  17. }
  18. }
  19. // end add
  20. var r = elm.attachEvent('on' + evType, IEfn);
  21. return r;
  22. }
  23. else {
  24. elm['on' + evType] = fn;
  25. }
  26. };

改造は8~18行目。ここで登録した関数を要素のメソッドとして呼び出すようにした。ついでに、EventオブジェクトにcurrentTargetも入れてみる。こうすれば、たとえばrowSelect.msOver()内のthisキーワードは呼び出し元の<tr>を参照する。

classの確認/登録/削除

  1. var hasClass = function(elm,cname){
  2. var classes = elm.className;
  3. if(!classes) return false;
  4. if(classes == cname) return true;
  5. return elm.className.search("\\b" + cname + "\\b") != -1;
  6. }
  7.  
  8. var addClass = function(elm,cname){
  9. if(hasClass(elm,cname)) return;
  10. if(elm.className) cname = " " + cname;
  11. elm.className += cname;
  12. }
  13.  
  14. var delClass = function(elm,cname){
  15. elm.className = elm.className.replace(new RegExp("\\b" + cname + "\\b\\s*","g"),"");
  16. }

classの切替は、サイ本16.5あたりを大いにパクr...もといインスパイアした、上記のような関数を利用する。

IE element.setAttribute("className", "foo");
Fx/Opera element.setAttribute("class", "foo");
IE/Fx/Opera element.className = "foo";

class関連も互換性の問題はあるけれど、elm.classNameで概ね対応できるので楽っちゃ楽。

課題

IEだと重いよ~。どうも<tr>内のmouseoutのたびに実行されてるdelClass()が原因のようだ。

サンプルみたく、<td>の中身がテキストのみか、せいぜい<p>がいくつかある程度のシンプルなテーブルならあんま問題にならないけど、画像が入ったり<span>が入ったりと中身が詰まってくると一気に重くなる。

何か良い方法はないものか? mouseoutイベントの発生そのものは抑えられないけど、なんらかの条件式を用いてdelClass()の実行を抑制できれば、ずいぶん軽くなるとは思うんだけど。教えてえらい人!

おまけ

今回の作業でイベント周りの挙動がだいぶハッキリしました。サイ本さまさま。特に17章は必読ですね。Webで5~6時間かけてようやくヒント掴んだくらいの問題が、サイ本5分で解決できたよ! 会社の方にも常備しようかと思います。

関連記事

Note: スパム対策が面倒なので、コメント投稿を廃止しました。以前のコメントは残します。
ご意見・ご要望はtwitter@sigwygかはてブコメントにて。