0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

エクセルLIKEなテーブルフィルタのつくりかた【Javascript】

Last updated at Posted at 2023-08-15

目次

はじめに
filter.js キック(vue created)
tFilterInit関数 解説
tFilterCreate関数 解説
tFilterAllSet関数 解説
tFilterClick関数 解説
tFilterGo関数 解説
tFilterCancel関数 解説
tFilterSave関数 解説
tFilterCloseOpen関数 解説
filter.js コード全文
ソースコード(Git)
参考サイト

はじめに

ここでは以下で紹介した家計簿アプリで実装したテーブルフィルタの実装方法について解説します。

開発ツールはvueを用いていて、フロントエンドのほとんどの処理はvueの中で実装しています。
テーブルフィルタについてだけは、区別して実装したかったので、filter.jsにまとめて実装しました。

filter.js キック(vue created)

ページ読み込み時にtFilterInit関数を実行させるようにvue側で指定しています。
axiosのリクエストで取得したデータベースのデータ(this.oeconomicas)を引数にして実行してます。

Oeconomica.vue
    created: async function () {
        try {
            const oResult = await axios.get("/oeconomica/",{
                params: {
                    year_month: this.year_month
                }
            });
            this.oeconomicas = oResult.data;
            this.registSubscription(); // サブスク自動登録
            this.getCategoryDatas();
            tFilterInit(this.oeconomicas);
        } catch (err) {
            console.log(err);
            alert(JSON.stringify(err));
        }
    },

vueのcreatedの仕組みは以下記事を参考ください。

tFilterInit関数 解説

フィルタ実装のメイン関数です。
ここでは、テーブルのカラムにボタンを表示させる処理が実装されています。

フローチャート

filter.js - tFilterInit(抜粋)
  // --- 「cmanFilterBtn」の定義があるセルを対象とする ------
  if(wTD[j].getAttribute('cmanFilterBtn') !== null){
    // --- フィルタ対象はボタンの次の行から -----------------
    gTfStartRow = i + 1;
    // --- ボタンを追加(画像はsvgを使用) ------------------
    wAddBtn  = '<div class="tfArea">';
    wAddBtn += '<svg class="tfImg" id="tsBtn_'+j+'" onclick="tFilterCloseOpen('+j+')"><path d="M0 0 L9 0 L6 4 L6 8 L3 8 L3 4Z"></path></svg>';
    wAddBtn += '<div class="tfList" id="tfList_'+j+'" style="display:none">';
    wAddBtn += tFilterCreate(j);
    wAddBtn += '</div>';
    wAddBtn += '</div>';
    wTD[j].innerHTML = wTD[j].innerHTML+wAddBtn;
フィルタを付与するカラムの指定方法

htmlの方で、thタグにcmanFilterBtnという属性を追記します。javascript側でそれを認識したら、ボタンを追加する処理に分岐される仕組みです。
次の場合だと、収支とカテゴリの項目にボタンが付けられます。

cmanFilterBtn定義
<tr>
    <th width="38">曜日</th>
    <th>日付</th>   
    <th cmanFilterBtn>収支</th>
    <th cmanFilterBtn>カテゴリ</th>
    <th>金額(円)</th>
    <th>メモ</th>
    <th width="80">変更</th>
    <th width="80">削除</th>
</tr>
ボタン描画

ボタンの見た目をsvgを用いて描いています。
<path d="M0 0 L9 0 L6 4 L6 8 L3 8 L3 4Z"></path>というコードを指定することで、
エクセルでお馴染みのマーク(逆三角フラスコ?)が描かれる仕組みです。

svgの定義方法は以下のサイトで詳しくまとまっています。

その他
  • onclick="tFilterCloseOpen('+j+')":ボタンがクリック時tFilterCloseOpen関数を実行
  • wAddBtn += tFilterCreate(j);:フィルタの詳細な設定をtFilterCreate関数で定義する

tFilterCreate関数 解説

この関数では、フィルタリスト(フィルタのボタンを押した時に開くメニュー)を定義するhtmlを作成し、実行元(tFilterInit)に返す処理が実装されています。

フローチャート

「すべて」のチェックボックスをクリックした時の処理

  • tFilterAllSet関数を実行 → 該当の列のチェックボックス全部のチェックをON/OFFさせる
「すべて」のチェックボックスのhtml
var wItemId = 'tfData_ALL_'+argCol;
rcList += '<div class="tfMeisai">';
rcList += '<input type="checkbox" id="'+wItemId+'" checked onclick="tFilterAllSet('+argCol+')">';
rcList += '<label for="'+wItemId+'">(すべて)</label>';
rcList += '</div>';

通常のチェックボックスをクリックした時の処理

チェックボックスのhtml
wItemId = 'tfData_'+argCol+'_r'+i;
rcList += '<div class="tfMeisai">';
rcList += '<input type="checkbox" id="'+wItemId+'" value="'+wVal+'" checked onclick="tFilterClick('+argCol+')">';
rcList += '<label for="'+wItemId+'">'+( wVal=='' ? '(空白)' : wVal )+'</label>';
rcList += '</div>';

「OK」、「Cancel」をクリックした時の処理

  • 「OK」クリック時:tFilterGo関数でフィルタ抽出実行
  • 「Cancel」クリック時:tFilterCancel関数でキャンセル時の処理を実行
OK,Cancelボタンのhtml
rcList += '<div class="tfBtnArea">';
rcList += '<input type="button" value="OK" onclick="tFilterGo()">';
rcList += '<input type="button" value="Cancel" onclick="tFilterCancel('+argCol+')">';
rcList += '</div>';

tFilterAllSet関数 解説

※ソースコードはfilter.js コード全文を参照

処理概要

  • 「すべて」のチェックボックスのチェックを付けた時、文字通り他のすべてのチェックボックスのチェックもつける
  • 「すべて」のチェックボックスのチェックを外した時、文字通り他のすべてのチェックボックスのチェックも外す

tFilterClick関数 解説

※ソースコードはfilter.js コード全文を参照

処理概要

  • チェックボックスのチェック数に応じて、「すべて」のチェックのON、OFFを調整する

フローチャート

tFilterGo関数 解説

※ソースコードはfilter.js コード全文を参照

フィルタリスト内の検索に応じてテーブルに対しフィルタを実行する関数です。

フローチャート

テーブルフィルタ機能それ自体のフローのため、少し多めな感じになりました。。。

属性付与によるテーブル行非表示の仕組み

上記、フローチャートにあるように、対象のtrタグに「cmanFilterNone」という属性を与えることで、
テーブルの行全体を非表示にし,また境界線を二重線にすることで行と行の間に非表示されたものがあることを示す演出となっています。

この処理は、cssで下記のように規定して実現させています。

style.css
/* === フィルタ非表示 ============================ */
#inOutTable tr[cmanFilterNone]{
    display    : none;
}
/* === フィルタ非表示行と次行の間を二重線にする == */
#inOutTable tr[cmanFilterNone] + tr td{
    border-top : 3px double #777;
}

フィルタ実行後の画面です。フィルタを行ったボタンの色が黄色になり、隠された部分は二重線で非表示になっていることがわかるようになっています。
スクリーンショット 2023-08-12 17.40.12.png

tFilterCancel関数 解説

※ソースコードはfilter.js コード全文を参照

処理概要

  • フィルタリストの「cancel」ボタンを押した時実行
  • フィルタリストの中の状態を、開いた時の状態に復元させる
    tFilterSave関数を実行させることで状態復元。
  • フィルタリストを閉じる

tFilterSave関数 解説

※ソースコードはfilter.js コード全文を参照

この関数では次のようなことを実現させます。

  • フィルタを閉じてもう一度開いた時、直前で設定した状態を維持したまま再表示させる。
    → これ無しだとフィルタを設定しても、また開いた時にデフォルト値に戻ってしまう。

フローチャート

tFilterCloseOpen関数 解説

※ソースコードはfilter.js コード全文を参照

実行タイミング
  • フィルタボタンをクリックしたタイミング
  • フィルタリストの「OK」、「キャンセル」ボタンを押したタイミング
処理フロー
  1. 開いている他のフィルタリストを全て閉じる
  2. クリックしたフィルタリストを開く(引数(クリックした列番号)が指定されている場合のみ)
  3. tFilterSave関数を実行しフィルタの設定(どこにチェックを付けたか)を保存する。
解説
  • フィルタリストの表示非表示はstyleの設定(display none)によって制御している。

  • 言わずもがなかもだが、全てを閉じる処理を入れないと、複数のフィルタが同時に開いてしまう動きになってしまう。この処理を含めることで、別のフィルタを開いている状態で新しいフィルタをクリックしたときに表示が切り替わるようになる。

filter.js コード全文

filter.js
//===============================================================
//  フィルタテーブルの共通変数 設定要!
//===============================================================
var gTabldID = 'inOutTable';  // テーブルのエリアのIDを設定
var gTfStartRow = 0;
var gTfColList  = [];             // ボタンが配置されている列番号
var gTfListSave = {};             // フィルタリストの保存状態
var varOeconomicas = '';
const colObj = {
  0:'day_of_week',
  1:'date',
  2:'balance',
  3:'category'
}

//===============================================================
//  オンロードでテーブル初期設定関数をCALL
//===============================================================
// window.onload = function() {
//   tFilterInit();
// }
const setOeconomicas = (oeconomicas) => {
  varOeconomicas = oeconomicas;
}

const tFilterInit = (oeconomicas) =>{
  
  //==============================================================
  //  テーブルの初期設定
  //==============================================================
  var wTABLE = document.getElementById(gTabldID);
  var wTR = wTABLE.rows;
  var wAddBtn = '';
  setOeconomicas(oeconomicas); //セッター
  // ------------------------------------------------------------
  //   テーブル内にフィルタボタンを付ける
  // ------------------------------------------------------------
  for(var i=0; i < wTR.length; i++){
    var wTD = wTABLE.rows[i].cells;

    for(var j=0; j < wTD.length; j++){
      
      // --- 「cmanFilterBtn」の定義があるセルを対象とする ------
      if(wTD[j].getAttribute('cmanFilterBtn') !== null){
  
        // --- フィルタ対象はボタンの次の行から -----------------
        gTfStartRow = i + 1;
  
        // --- ボタンを追加(画像はsvgを使用) ------------------
        wAddBtn  = '<div class="tfArea">';
        wAddBtn += '<svg class="tfImg" id="tsBtn_'+j+'" onclick="tFilterCloseOpen('+j+')"><path d="M0 0 L9 0 L6 4 L6 8 L3 8 L3 4Z"></path></svg>';
        wAddBtn += '<div class="tfList" id="tfList_'+j+'" style="display:none">';
        wAddBtn += tFilterCreate(j);
        wAddBtn += '</div>';
        wAddBtn += '</div>';
        wTD[j].innerHTML = wTD[j].innerHTML+wAddBtn;
  
        // --- フィルタボタンなる列を保存 -----------------------
        gTfColList.push(j);
      }
    }
     // --- ボタンを付けたら以降の行は無視する -------------------
     if(wAddBtn != ''){
       var gSortBtnRow = i;
       break;
    }
  }
}
 
 function tFilterCreate(argCol){
  //==============================================================
  //  指定列のフィルタリスト作成
  //==============================================================
  var wItem     = [];              // クリックされた列の値
  var wNotNum   = 0;               // 1 : 数字でない
  var wItemSave = {};              // フィルタに設定した値がキー
  var rcList    = '';              // 返すフィルタリスト
  var wVal = '';
  // ------------------------------------------------------------
  //  クリックされた列の値を取得する
  //------------------------------------------------------------
  for(var i=gTfStartRow; i < varOeconomicas.length; i++){
    var j = i - gTfStartRow;
    wItem[j] = varOeconomicas[i][colObj[argCol]];
    if(wItem[j].match(/^[-]?[0-9,\.]+$/)){
    }else{
      wNotNum = 1;
    }
  }
   
   // ------------------------------------------------------------
   //  列の値でソートを実行
   // ------------------------------------------------------------
     if(wNotNum == 0){
       wItem.sort(sortNumA);           // 数値で昇順
     }else{
       wItem.sort(sortStrA);           // 文字で昇順
     }
   
   // ------------------------------------------------------------
   //  「すべて」のチェックボックス作成
   // ------------------------------------------------------------
   var wItemId = 'tfData_ALL_'+argCol;
   rcList += '<div class="tfMeisai">';
   rcList += '<input type="checkbox" id="'+wItemId+'" checked onclick="tFilterAllSet('+argCol+')">';
   rcList += '<label for="'+wItemId+'">(すべて)</label>';
   rcList += '</div>';
  
   // ------------------------------------------------------------
   //  列の値でフィルタのチェックボックスを作成する
   //    チェックボックスはformで囲む
   // ------------------------------------------------------------
   rcList += '<form name="tfForm_'+argCol+'">';
  
   for(var i=0; i < wItem.length; i++){
  
     wVal = trim(wItem[i]);
     
     if(wVal in wItemSave){
       // ---値でチェックボックスが作成されている(重複) ----------
     }else{  
       // ---チェックボックスの作成 ------------------------------
       wItemId = 'tfData_'+argCol+'_r'+i;
       rcList += '<div class="tfMeisai">';
       rcList += '<input type="checkbox" id="'+wItemId+'" value="'+wVal+'" checked onclick="tFilterClick('+argCol+')">';
       rcList += '<label for="'+wItemId+'">'+( wVal=='' ? '(空白)' : wVal )+'</label>';
       rcList += '</div>';
  
       // ---重複判定用にチェックボックスの値を保存 --------------
       wItemSave[wVal]='1';
     }
   }
   rcList += '</form>';
   
   // ------------------------------------------------------------
   //  文字抽出のinputを作成
   // ------------------------------------------------------------
   rcList += '<div class="tfInStr">';
   rcList += '<input type="text" placeholder="含む文字抽出" id="tfInStr_'+argCol+'">';
   rcList += '</div>';
  
   // ------------------------------------------------------------
   //  「OK」「Cancel」ボタンの作成
   // ------------------------------------------------------------
   rcList += '<div class="tfBtnArea">';
   rcList += '<input type="button" value="OK" onclick="tFilterGo()">';
   rcList += '<input type="button" value="Cancel" onclick="tFilterCancel('+argCol+')">';
   rcList += '</div>';
   // ------------------------------------------------------------
   //  作成したhtmlを返す
   // ------------------------------------------------------------
   return rcList;
 }
 
 function tFilterClick(argCol){
  //==============================================================
  //  フィルタリストのチェックボックスクリック
  //    「すべて」のチェックボックスと整合性を合わせる
  //==============================================================
   var wForm   = document.forms['tfForm_'+argCol];
   var wCntOn  = 0;
   var wCntOff = 0;
   var wAll    = document.getElementById('tfData_ALL_'+argCol);   // 「すべて」のチェックボックス
  
   // --- 各チェックボックスの状態を集計する ---------------------
   for (var i = 0; i < wForm.elements.length; i++){
     if(wForm.elements[i].type == 'checkbox'){
       if (wForm.elements[i].checked) { wCntOn++;  }
       else                           { wCntOff++; }
     }
   }
  
   // --- 各チェックボックス集計で「すべて」を整備する -----------
   if((wCntOn == 0)||(wCntOff == 0)){
     wAll.checked = true;             // 「すべて」をチェックする
     tFilterAllSet(argCol);           // 各フィルタのチェックする
   }else{
      wAll.checked = false;           // 「すべて」をチェックを外す
   }
 }
 
 function tFilterCancel(argCol){
  //==============================================================
  //  キャンセルボタン押下
  //==============================================================
   tFilterSave(argCol, 'load');    // フィルタ条件の復元
   tFilterCloseOpen('');           // フィルタリストを閉じる
  
 }
 
function tFilterGo(){
  //===============================================================
  //  フィルタの実行
  //===============================================================
  var wTABLE  = document.getElementById(gTabldID);
  var wTR     = wTABLE.rows;
  
  // ------------------------------------------------------------
  //  全ての非表示を一旦クリア
  // ------------------------------------------------------------
  for(var i = 0; i < wTR.length; i++){
    if(wTR[i].getAttribute('cmanFilterNone') !== null){
      wTR[i].removeAttribute('cmanFilterNone');
    }
  }
  // ------------------------------------------------------------
  //  フィルタボタンのある列を繰り返す
  // ------------------------------------------------------------
  for(var wColList = 0; wColList < gTfColList.length; wColList++){
    var wCol       = gTfColList[wColList];
    var wAll       = document.getElementById('tfData_ALL_'+wCol);     // 「すべて」のチェックボックス
    var wItemSave  = {};
    var wFilterBtn =  document.getElementById('tsBtn_'+wCol);
    var wFilterStr =  document.getElementById('tfInStr_'+wCol);
    var wForm      = document.forms['tfForm_'+wCol];
    var wVal = '';

    // -----------------------------------------------------------
    //  チェックボックスの整備(「すべて」の整合性)
    // -----------------------------------------------------------
    for (var i = 0; i < wForm.elements.length; i++){
      if(wForm.elements[i].type == 'checkbox'){
        if (wForm.elements[i].checked) {
          wItemSave[wForm.elements[i].value] = 1;      // チェックされている値を保存
        }
      }
    }
     // -----------------------------------------------------------
     //  フィルタ(非表示)の設定
     // -----------------------------------------------------------
    if((wAll.checked)&&(trim(wFilterStr.value) == '')){
      wFilterBtn.style.backgroundColor = '';              // フィルタなし色
    }
    else{
      wFilterBtn.style.backgroundColor = '#ffff00';       // フィルタあり色
  
      for(var i=gTfStartRow; i < wTR.length; i++){
        wVal = trim(varOeconomicas[i-1][colObj[wCol]]);
        // --- チェックボックス選択によるフィルタ ----------------
        if(!wAll.checked){
          if(wVal in wItemSave){
          }
          else{
            wTR[i].setAttribute('cmanFilterNone','');
          }
        }
  
         // --- 抽出文字によるフィルタ ----------------------------
        if(wFilterStr.value != ''){
          reg = new RegExp(wFilterStr.value);
          if(wVal.match(reg)){
          }
          else{
            wTR[i].setAttribute('cmanFilterNone','');
          }
        }
      }
    }
  }
  tFilterCloseOpen('');
 }
 
 function tFilterSave(argCol, argFunc){
  //==============================================================
  //  フィルタリストの保存または復元
  //==============================================================
   // ---「すべて」のチェックボックス値を保存 ------------------
   var wAllCheck = document.getElementById('tfData_ALL_'+argCol);
   if(argFunc == 'save'){
     gTfListSave[wAllCheck.id] = wAllCheck.checked;
   }else{
     wAllCheck.checked = gTfListSave[wAllCheck.id];
   }
  
   // --- 各チェックボックス値を保存 ---------------------------
   var wForm    = document.forms['tfForm_'+argCol];
   for (var i = 0; i < wForm.elements.length; i++){
     if(wForm.elements[i].type == 'checkbox'){
       if(argFunc == 'save'){
         gTfListSave[wForm.elements[i].id] = wForm.elements[i].checked;
       }else{
         wForm.elements[i].checked = gTfListSave[wForm.elements[i].id];
       }
     }
   }
  
   // --- 含む文字の入力を保存 ---------------------------------
   var wStrInput = document.getElementById('tfInStr_'+argCol);
   if(argFunc == 'save'){
     gTfListSave[wStrInput.id] = wStrInput.value;
   }else{
     wStrInput.value = gTfListSave[wStrInput.id];
   }
 }
 
const tFilterCloseOpen = (argCol) => {
  //==============================================================
  //  フィルタを閉じて開く
  //==============================================================
  // --- フィルタリストを一旦すべて閉じる -----------------------
  for(var i=0; i < gTfColList.length; i++){
    document.getElementById("tfList_"+gTfColList[i]).style.display = 'none';
  }
  // --- 指定された列のフィルタリストを開く ---------------------
  if(argCol != ''){
    document.getElementById("tfList_"+argCol).style.display = '';
    // --- フィルタ条件の保存(キャンセル時に復元するため) -----
    tFilterSave(argCol, 'save');
  
  }
}

function tFilterAllSet(argCol){
//==============================================================
//  「すべて」のチェック状態に合わせて、各チェックをON/OFF
//==============================================================
  var wChecked = false;
  var wForm    = document.forms['tfForm_'+argCol];

  if(document.getElementById('tfData_ALL_'+argCol).checked){
    wChecked = true;
  }

  for (var i = 0; i < wForm.elements.length; i++){
    if(wForm.elements[i].type == 'checkbox'){
      wForm.elements[i].checked = wChecked;
    }
  }
}

function tFilterReset(){
  //==============================================================
  //  フィルタを削除する
  //==============================================================
  let elements = document.querySelectorAll('.tfArea');
  elements.forEach((element) => {
    element.remove();
  });
}
  
 function sortNumA(a, b) {
  //==============================================================
  //  数字のソート関数(昇順)
  //==============================================================
   a = parseInt(a.replace(/,/g, ''));
   b = parseInt(b.replace(/,/g, ''));
  
   return a - b;
 }
 
 function sortStrA(a, b){
  //==============================================================
  //  文字のソート関数(昇順)
  //==============================================================
   a = a.toString().toLowerCase();  // 英大文字小文字を区別しない
   b = b.toString().toLowerCase();
  
   if     (a < b){ return -1; }
   else if(a > b){ return  1; }
   return 0;
 }
 
 function trim(argStr){
  //==============================================================
  //  trim
  //==============================================================
   var rcStr = argStr;
   rcStr	= rcStr.replace(/^[  \r\n]+/g, '');
   rcStr	= rcStr.replace(/[  \r\n]+$/g, '');
   return rcStr;
 }

ソースコード(Git)

参考サイト

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?