JavaScriptによるライブラリ不要のテーブルソート

はてなブログJavaScriptが使えるそうなので、いろいろ試してみたいと思います。

目標

  •  特殊なライブラリを使用せずにテーブルソートを行う
  • ソートするデータは静的コンテンツで、JSON形式とする
  • 条件設定枠を設けて表示データを絞り込めるようにする
  • なるべくシンプルに、汎用性を高める
  • 最終目標はパズドラの究極データベースのようなもの

そんな感じです。

成果物

NoNameHPAttackHeal

主要なブラウザでだいたい動くと思います。

ソース

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>json - table</title>
<script type="text/javascript">
<!--
/**
* データ定義
*/
var tableData = [
{"No":1,"Name":"aa","HP":200,"Attack":300,"Heal":100},
{"No":2,"Name":"bb","HP":100,"Attack":200,"Heal":300},
{"No":3,"Name":"cc","HP":300,"Attack":200,"Heal":100},
{"No":4,"Name":"dd","HP":400,"Attack":150,"Heal":50},
{"No":5,"Name":"ee","HP":500,"Attack":50,"Heal":50},
{"No":6,"Name":"ff","HP":350,"Attack":450,"Heal":-150}
]; // 実際のデータ
var sortKey = ["No", "HP", "Attack", "Heal"]; // ソート項目
var asc = false; // 昇順(true)/降順(false)
var nowSortKey = "No"; // 現在ソートキー

/**
* テーブルデータ生成
*/
function createTable(obj) {
var sort = "No"; // デフォルトソート
if (obj != undefined && obj.id != undefined) {
sort = obj.id;
var sortFlg = false;
for (var i=0; i<sortKey.length; i++) {
if (sortKey[i] == sort) {
sortFlg = true;
break;
} else {
continue;
}
}
if (!sortFlg) return false; // ソート項目でなければ処理スキップ
}
asc = !asc; // 昇順/降順切り替え
if (nowSortKey != sort) {
asc = true; // ソート項目変更時は昇順
nowSortKey = sort;
}
// ヘッダー編集
editHeader();

var tbodyElm = document.getElementById("tbody_detail");
// tbody配下クリア
deleteTable(tbodyElm);
// データをソート
tableData.sort(
function (a, b) {
var aName = a[sort];
var bName = b[sort];
if (asc) return (aName > bName) ? 1 : -1;
else return (aName < bName) ? 1 : -1;
}
);
// tbody配下再作成
for (var i=0; i<tableData.length; i++) {
tbodyElm.appendChild(createTrElement(tableData[i]));
}
}
/**
* テーブルデータ削除
*/
function deleteTable(tbodyElm) {
// 全ての子ノードを削除
for (var i=tbodyElm.childNodes.length-1; i>=0; i--) {
tbodyElm.removeChild(tbodyElm.childNodes[i]);
}
}
/**
* <tr>タグ生成
*/
function createTrElement(data) {
var trElm = document.createElement("tr");
trElm.appendChild(createTdElement(data.No));
trElm.appendChild(createTdElement(data.Name));
trElm.appendChild(createTdElement(data.HP));
trElm.appendChild(createTdElement(data.Attack));
trElm.appendChild(createTdElement(data.Heal));
return trElm;
}
/**
* <td>タグ生成
*/
function createTdElement(txt) {
var tdElm = document.createElement("td");
var txtObj = document.createTextNode(txt);
tdElm.appendChild(txtObj);
return tdElm;
}
/**
* テーブルヘッダー編集
*/
function editHeader() {
// ヘッダー初期化
for (var i=0; i<sortKey.length; i++) {
var obj = document.getElementById(sortKey[i]);
obj.style.color = "#000000";
obj.removeChild(obj.childNodes[0]);
obj.appendChild(document.createTextNode(sortKey[i]));
}
// ソート項目の編集
if (nowSortKey != undefined && nowSortKey != "") {
var sortObj = document.getElementById(nowSortKey);
sortObj.style.color = "#0000FF";
sortObj.removeChild(sortObj.childNodes[0]);
var txt = (asc) ? "▼" : "▲";
sortObj.appendChild(document.createTextNode(nowSortKey + txt));
}
}
/**
* onloadイベント付与
*/
if (window.addEventListener) { //for W3C DOM
window.addEventListener("load", createTable, false);
} else if (window.attachEvent) { //for IE
window.attachEvent("onload", createTable);
} else {
window.onload = createTable;
}
//-->
</script>
</head>
<body>

<table border="1">
<thead id="thead_header">
<tr id="tr_header">
<th><span id="No" onclick="javascript:createTable(this);">No</span></th>
<th><span id="Name" onclick="javascript:createTable(this);">Name</span></th>
<th><span id="HP" onclick="javascript:createTable(this);">HP</span></th>
<th><span id="Attack" onclick="javascript:createTable(this);">Attack</span></th>
<th><span id="Heal" onclick="javascript:createTable(this);">Heal</span></th>
</tr>
</thead>
<tbody id="tbody_detail">
</tbody>
</table>

</body>
</html>

解説

主にJavaScriptの部分を見ていきます。

データ定義

  var tableData = [
    {"No":1,"Name":"aa","HP":200,"Attack":300,"Heal":100},
    {"No":2,"Name":"bb","HP":100,"Attack":200,"Heal":300},
    {"No":3,"Name":"cc","HP":300,"Attack":200,"Heal":100},
    {"No":4,"Name":"dd","HP":400,"Attack":150,"Heal":50},
    {"No":5,"Name":"ee","HP":500,"Attack":50,"Heal":50},
    {"No":6,"Name":"ff","HP":350,"Attack":450,"Heal":-150}
  ]; // 実際のデータ
  var sortKey = ["No", "HP", "Attack", "Heal"]; // ソート項目
  var asc = false; // 昇順(true)/降順(false)
  var nowSortKey = "No"; // 現在ソートキー

「tableData」は今回のメインとなるJSON形式のデータです。

「sortKey」はソート対象とする項目です。ここから値を減らしたり、"Name"を増やしたりすればソート可/不可が切り替わります。

初期ソートを"No"としてしまっているので、これを消すと挙動がきもちわるくなります。

「asc」は昇順/降順です。コメントの通りです。

「nowSortKey」は現在のソート項目です。ヘッダーの色を変えたりするのに使ってます。

テーブルデータ生成

  function createTable(obj) {
    var sort = "No"; // デフォルトソート
    if (obj != undefined && obj.id != undefined) {
      sort = obj.id;
      var sortFlg = false;
      for (var i=0; i<sortKey.length; i++) {
        if (sortKey[i] == sort) {
          sortFlg = true;
          break;
        } else {
          continue;
        }
      }
      if (!sortFlg) return false; // ソート項目でなければ処理スキップ
    }
    asc = !asc; // 昇順/降順切り替え
    if (nowSortKey != sort) {
      asc = true; // ソート項目変更時は昇順
      nowSortKey = sort;
    }
    // ヘッダー編集
    editHeader();

    var tbodyElm = document.getElementById("tbody_detail");
    // tbody配下クリア
    deleteTable(tbodyElm);
    // データをソート
    tableData.sort(
      function (a, b) {
        var aName = a[sort];
        var bName = b[sort];
        if (asc) return (aName > bName) ? 1 : -1;
        else return (aName < bName) ? 1 : -1;
      }
    );
    // tbody配下再作成
    for (var i=0; i<tableData.length; i++) {
      tbodyElm.appendChild(createTrElement(tableData[i]));
    }
  }

メイン処理です。

関数の冒頭はいろいろ面倒くさいことをやってますが、ここは後々シンプルにしておきたいです。

内容としては、ソート項目でないところをクリックされたら無視する処理です。

その後の処理では、クリックのたびに昇順/降順を反転させています。

ただし、ソート項目が変更された場合は常に昇順にする処理も行います。

ヘッダー編集は別関数に切り分けているので後述します。

コンテンツ部分を生成するtbodyタグを取得し、いったん子ノードを全て削除します。

その後、データをソート項目および昇順/降順に従ってソートします。ソート処理については以下を参考にさせてもらいました。連想配列のソートです。

JavaScriptの配列を特定の順番にソートする方法|WEB Drawer

最後にソートしたデータを使ってtbody配下を再作成しています。

テーブルデータ削除

  function deleteTable(tbodyElm) {
    // 全ての子ノードを削除
    for (var i=tbodyElm.childNodes.length-1; i>=0; i--) {
      tbodyElm.removeChild(tbodyElm.childNodes[i]);
    }
  }

こちらを参考にさせてもらいました。

子ノードを全て削除する (removeChild)

<tr>タグ生成

  function createTrElement(data) {
    var trElm = document.createElement("tr");
    trElm.appendChild(createTdElement(data.No));
    trElm.appendChild(createTdElement(data.Name));
    trElm.appendChild(createTdElement(data.HP));
    trElm.appendChild(createTdElement(data.Attack));
    trElm.appendChild(createTdElement(data.Heal));
    return trElm;
  }

createElementで<tr>タグを生成し、<td>タグをappendChildで子ノードとして追加しています。

<td>タグは次の関数で生成しています。

<td>タグ生成

  function createTdElement(txt) {
    var tdElm = document.createElement("td");
    var txtObj = document.createTextNode(txt);
    tdElm.appendChild(txtObj);
    return tdElm;
  }

<tr>タグと同様、createElementで<td>タグを生成しています。

子ノードとしてテキストノードを生成し、appendChildで追加しています。

引数として、<td>タグ内の文字列を受け取るようにしました。

テーブルヘッダー編集

  function editHeader() {
    // ヘッダー初期化
    for (var i=0; i<sortKey.length; i++) {
      var obj = document.getElementById(sortKey[i]);
      obj.style.color = "#000000";
      obj.removeChild(obj.childNodes[0]);
      obj.appendChild(document.createTextNode(sortKey[i]));
    }
    // ソート項目の編集
    if (nowSortKey != undefined && nowSortKey != "") {
      var sortObj = document.getElementById(nowSortKey);
      sortObj.style.color = "#0000FF";
      sortObj.removeChild(sortObj.childNodes[0]);
      var txt = (asc) ? "▼" : "▲";
      sortObj.appendChild(document.createTextNode(nowSortKey + txt));
    }
  }

順番が前後していますが、メイン処理で最初に呼び出されています。

思いつきのおまけとして追加したので最後にしちゃっています。

現在のソート項目を青字にし、▲▼で昇順/降順を示すようにしました。

処理としては、データ定義でソート項目として宣言している「sortKey」を全て黒字で初期化したあと、「nowSortKey」の部分を青字にし、「asc」で昇順/降順を判定して記号を追加しています。

onloadイベント付与

  if (window.addEventListener) { //for W3C DOM
    window.addEventListener("load", createTable, false);
  } else if (window.attachEvent) { //for IE
    window.attachEvent("onload", createTable);
  } else  {
    window.onload = createTable;
  }

これはさらにおまけです。

元々、静的コンテンツとしてローカルでhtmlファイルを作成して、単純に<body>タグでonloadから呼び出すようにしていましたが、それだとブログに貼り付けられそうにありませんでした。。

そのため、イベントを後付けしています。こちらを参考にさせてもらいました。

イベントに処理を追加する - JavaScript

また、トップ画面で流し読みした際にいちいち読み込まれるのも何かいやだったので、"続きを読む"で個別記事に入ったときだけ読み込まれるようにしています。

これは、同じような記事を書いたときに変数名が万が一にもバッティングしないよう、処理を個別記事の中に追いやるのも兼ねています。

今後の改良点

  • 当然ながら、絞込み機能を設ける
  • 初期ソート周りを改善する
  • メイン処理の中身をもっとシンプルにする

なお、JSONファイルの外部化は既にできているんですが、JSONファイルを置く場所が思いつかなかったので、今回はデータ定義で宣言しています。