JavaScriptによるライブラリ不要の絞込み機能付きテーブルソート

前回の続きです。

目標

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

そんな感じです。

成果物

流石に規模が大きくなってきたので以前に登録したfc2サービスに外だししました。

お手数ですがこちらをご参照くださいませ。

今回はひとまず、手持ちのモンスターをデータ化してみました。

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

ソース

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>json - table</title>
<script type="text/javascript">
<!--
/**
* データ定義
*/
var dataUrl = "https://dl.dropboxusercontent.com/u/9209131/share/data/mdata_130826.json"; // JSONデータURL
var sortKey = ["HP", "Attack", "Heal", "Total"]; // ソート項目
var asc = false; // 昇順(true)/降順(false)
var nowSortKey = ""; // 現在ソートキー
var initSortKey = "Total"; // 初期ソートキー
var refineAttr = [true, false, false, false, false, false]; // 条件(属性)[全て、火、水、木、光、闇]
var refineType = [true, false, false, false, false, false, false, false];
// 条件(タイプ)[全て、ドラゴン、バランス、体力、回復、攻撃、神、悪魔]
var dispNum = 25; // 表示件数

/**
* データ取得
*/
function initData(obj) {
var httpObj = null;
if(window.ActiveXObject) {
httpObj = new ActiveXObject('Microsoft.XMLHTTP'); //for IE
} else {
httpObj = new XMLHttpRequest();
}
httpObj.open("post", dataUrl, true);
httpObj.send(null);
httpObj.onreadystatechange = function() {
if(httpObj.readyState==4 && (httpObj.status==200 || true)) {
var data = JSON.parse(httpObj.responseText);
createTable(obj, data); // テーブルデータ生成
}
}
}
/**
* テーブルデータ生成
*/
function createTable(obj, data) {
var sort = initSortKey;
if (obj != undefined && obj.id != undefined) {
sort = obj.id;
}
// ソート項目判定
if (!isEnableSort(sort)) {
return false; // ソート項目でなければ処理終了
}
asc = !asc; // 昇順/降順切り替え
if (nowSortKey != sort) {
asc = false; // ソート項目変更時は降順
nowSortKey = sort;
}
// ヘッダー編集
editHeader();

var tbodyElm = document.getElementById("tbody_detail");
// tbody配下クリア
deleteTable(tbodyElm);
// データを絞込み
data = refineData(data);
// データをソート
data.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<data.length; i++) {
tbodyElm.appendChild(createTrElement(data[i]));
if (i == dispNum) {
break;
}
}
}
/**
* ソート項目判定
*/
function isEnableSort(sort) {
var sortFlg = false;
for (var i=0; i<sortKey.length; i++) {
if (sortKey[i] == sort) {
sortFlg = true;
break;
}
}
return sortFlg;
}
/**
* テーブルヘッダー編集
*/
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));
}
}
/**
* テーブルデータ削除
*/
function deleteTable(tbodyElm) {
// 全ての子ノードを削除
for (var i=tbodyElm.childNodes.length-1; i>=0; i--) {
tbodyElm.removeChild(tbodyElm.childNodes[i]);
}
}
/**
* データ絞込み
*/
function refineData(data) {
data = refineDataAttr(data); // 属性
data = refineDataType(data); // タイプ
return data;
}
/**
* データ絞込み(属性)
*/
function refineDataAttr(data) {
if (refineAttr[0]) {
// 条件「全て」が選択されている場合、全データ表示
return data;
}
var result = new Array();
var cnt = 0;
for (var i=0; i<data.length; i++) {
if )((refineAttr[1] && (data[i]["AttrMain"] == '火' || data[i]["AttrSub"] == '火'))(
|| (refineAttr[2] && (data[i]["AttrMain"] == '水' || data[i]["AttrSub"] == '水'))
|| (refineAttr[3] && (data[i]["AttrMain"] == '木' || data[i]["AttrSub"] == '木'))
|| (refineAttr[4] && (data[i]["AttrMain"] == '光' || data[i]["AttrSub"] == '光'))
|| (refineAttr[5] && (data[i]["AttrMain"] == '闇' || data[i]["AttrSub"] == '闇'))) {
result[cnt] = data[i];
cnt++;
continue;
}
}
return result;
}
/**
* データ絞込み(タイプ)
*/
function refineDataType(data) {
if (refineType[0]) {
// 条件「全て」が選択されている場合、全データ表示
return data;
}
var result = new Array();
var cnt = 0;
for (var i=0; i<data.length; i++) {
if )((refineType[1] && (data[i]["Type1"] == 'ドラゴン' || data[i]["Type2"] == 'ドラゴン'))(
|| (refineType[2] && (data[i]["Type1"] == 'バランス' || data[i]["Type2"] == 'バランス'))
|| (refineType[3] && (data[i]["Type1"] == '体力' || data[i]["Type2"] == '体力'))
|| (refineType[4] && (data[i]["Type1"] == '回復' || data[i]["Type2"] == '回復'))
|| (refineType[5] && (data[i]["Type1"] == '攻撃' || data[i]["Type2"] == '攻撃'))
|| (refineType[6] && (data[i]["Type1"] == '神' || data[i]["Type2"] == '神'))
|| (refineType[7] && (data[i]["Type1"] == '悪魔' || data[i]["Type2"] == '悪魔'))) {
result[cnt] = data[i];
cnt++;
continue;
}
}
return result;
}
/**
* <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));
trElm.appendChild(createTdElement(data.Total));
trElm.appendChild(createTdElement(data.Lv));
trElm.appendChild(createTdElement(data.SLv));
trElm.appendChild(createTdElement(data.Cost));
return trElm;
}
/**
* <td>タグ生成
*/
function createTdElement(txt) {
var tdElm = document.createElement("td");
var txtObj = document.createTextNode(txt);
tdElm.appendChild(txtObj);
if (txt == "MAX") {
tdElm.style.color = "#0000ff";
}
return tdElm;
}
/**
* onloadイベント付与
*/
if (window.addEventListener) { //for W3C DOM
window.addEventListener("load", initData, false);
} else if (window.attachEvent) { //for IE
window.attachEvent("onload", initData);
} else {
window.onload = initData;
}
/**
* 絞込み条件設定
*/
function editCondition(obj) {
var refineArray = obj.id.split('_');
this[refineArray[0]][refineArray[1]] = !this[refineArray[0]][refineArray[1]];
if (refineArray[1] == '0') {
// 条件「全て」選択時
if (this[refineArray[0]][refineArray[1]]) {
resetCondition(refineArray[0]);
obj.className += " cnBtn_on";
for (var i=1; i<this[refineArray[0]].length; i++) {
document.getElementById(refineArray[0] + "_" + i).className = "cnBtn";
}
} else {
return false;
}
} else if (this[refineArray[0]][refineArray[1]]) {
obj.className += " cnBtn_on";
document.getElementById(refineArray[0] + "_0").className = "cnBtn";
this[refineArray[0]][0] = false;
} else {
obj.className = "cnBtn";
this[refineArray[0]][0] = true;
document.getElementById(refineArray[0] + "_0").className += " cnBtn_on";
for (var i=1; i<this[refineArray[0]].length; i++) {
if (this[refineArray[0]][i]) {
this[refineArray[0]][0] = false;
document.getElementById(refineArray[0] + "_0").className = "cnBtn";
break;
}
}
}
asc = !asc;
var sortObj = new Object();
sortObj.id = nowSortKey;
initData(sortObj);
}
/**
* 絞込み条件初期化
*/
function resetCondition(condition) {
if (condition == "refineAttr") {
refineAttr = [true, false, false, false, false, false];
} else if (condition == "refineType") {
refineType = [true, false, false, false, false, false, false, false];
}
}
//-->
</script>
<style type="text/css">
<!--
#table_condition ul{
margin: 0;
padding: 0;
list-style: none;
}
#table_condition li{
display: inline;
padding: 0;
margin: 0;
float: left;
}
#table_condition li span.cnBtn{
display: block;
width: 60px;
padding: 3px;
margin: 10px 0px 10px 0px;
text-decoration: none;
border:outset 3px #00a3db;
background-color: #00a3db;
text-align: center;
color: #000000;
font-size: 14px;
}
#table_condition li span.cnBtn_on{
border: inset 3px #4b64a1;
background-color: #4b64a1;
color: #ffffff;
}
//-->
</style>
</head>
<body>

<div id="table_condition">
<ul>
<li><span id="refineAttr_0" class="cnBtn cnBtn_on" onclick="javascript:editCondition(this);">全て</span>
<li><span id="refineAttr_1" class="cnBtn" onclick="javascript:editCondition(this);">火</span>
<li><span id="refineAttr_2" class="cnBtn" onclick="javascript:editCondition(this);">水</span>
<li><span id="refineAttr_3" class="cnBtn" onclick="javascript:editCondition(this);">木</span>
<li><span id="refineAttr_4" class="cnBtn" onclick="javascript:editCondition(this);">光</span>
<li><span id="refineAttr_5" class="cnBtn" onclick="javascript:editCondition(this);">闇</span>
</ul>
<ul style="clear:left;">
<li><span id="refineType_0" class="cnBtn cnBtn_on" onclick="javascript:editCondition(this);">全て</span>
<li><span id="refineType_1" class="cnBtn" onclick="javascript:editCondition(this);">ドラゴン</span>
<li><span id="refineType_2" class="cnBtn" onclick="javascript:editCondition(this);">バランス</span>
<li><span id="refineType_3" class="cnBtn" onclick="javascript:editCondition(this);">体力</span>
<li><span id="refineType_4" class="cnBtn" onclick="javascript:editCondition(this);">回復</span>
<li><span id="refineType_5" class="cnBtn" onclick="javascript:editCondition(this);">攻撃</span>
<li><span id="refineType_6" class="cnBtn" onclick="javascript:editCondition(this);">神</span>
<li><span id="refineType_7" class="cnBtn" onclick="javascript:editCondition(this);">悪魔</span>
</ul>
</div>
<br /><br />
<table border="1" style="clear:left;">
<thead id="thead_header">
<tr id="tr_header">
<th><span id="No" onclick="javascript:initData(this);">No</span></th>
<th><span id="Name" onclick="javascript:initData(this);">Name</span></th>
<th><span id="HP" onclick="javascript:initData(this);">HP</span></th>
<th><span id="Attack" onclick="javascript:initData(this);">Attack</span></th>
<th><span id="Heal" onclick="javascript:initData(this);">Heal</span></th>
<th><span id="Total" onclick="javascript:initData(this);">Total</span></th>
<th><span id="Lv" onclick="javascript:initData(this);">Lv</span></th>
<th><span id="SLv" onclick="javascript:initData(this);">SLv</span></th>
<th><span id="Cost" onclick="javascript:initData(this);">Cost</span></th>
</tr>
</thead>
<tbody id="tbody_detail">
</tbody>
</table>

</body>
</html>

解説

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

前回とほとんど変更が無い部分に関しては記載しておりません。

前回の記事をご参照ください。

データ定義

  var dataUrl = "https://dl.dropboxusercontent.com/u/9209131/share/data/mdata_130826.json"; // JSONデータURL
var sortKey = ["HP", "Attack", "Heal", "Total"]; // ソート項目
var asc = false; // 昇順(true)/降順(false)
var nowSortKey = ""; // 現在ソートキー
var initSortKey = "Total"; // 初期ソートキー
var refineAttr = [true, false, false, false, false, false]; // 条件(属性)[全て、火、水、木、光、闇]
var refineType = [true, false, false, false, false, false, false, false];
// 条件(タイプ)[全て、ドラゴン、バランス、体力、回復、攻撃、神、悪魔]
var dispNum = 25; // 表示件数

「dataUrl」は今回のメインとなるJSON形式のデータです。前回と違い、外部ファイル化しています。

我らがDropboxがダイレクトリンクという機能を持っていたので利用してみることにしました。

実装に当たってはこちらを参考にさせてもらいました。

シケモク Tech: Dropboxで廃止されたPublicフォルダ機能を5秒で復活させる方法

大変便利な機能ですが、日毎の通信量制限があるそうなのでご注意ください。

「initSortKey」は初期ソートキーです。今のところ初期表示時以外では使いません。

ソートについてはいろいろ修正しました。よく考えたらランキングは降順が標準ですね。

「refineAttr」「refineType」は今回追加した絞込み機能のON/OFFを持つ配列です。

「dispNum」は急遽足した表示件数です。これは次に手動操作可能にしたいと思います。

データ取得

  function initData(obj) {
var httpObj = null;
if(window.ActiveXObject) {
httpObj = new ActiveXObject('Microsoft.XMLHTTP'); //for IE
} else {
httpObj = new XMLHttpRequest();
}
httpObj.open("post", dataUrl, true);
httpObj.send(null);
httpObj.onreadystatechange = function() {
if(httpObj.readyState==4 && (httpObj.status==200 || true)) {
var data = JSON.parse(httpObj.responseText);
createTable(obj, data); // テーブルデータ生成
}
}
}

JSONデータを外部ファイル化し、必要なときに取得するようにしています。

通信量を考えると、一度どこかに格納してあげたほうが良いかもしれません。

JSONデータの読み込みについては、こちらを参考にさせてもらいました。

JSONデータを解析して読み込む

なお、IE用に若干、修正を入れています。こちらを参考にさせてもらいました。

Ajaxを実行するときIEだけで発生するエラー | 言葉の海のプログラマー

また、データ定義の「dataUrl」でローカルファイルのパスを指定することもできますが、その場合はブラウザによって動かなくなるケースもあります。

自分の環境ではGoogle Chrome、IE8がブラウザ制限で動きませんでした。

参考:ローカルのHTMLファイルからどこまで読み取れるか選手権 2011 - subtech

テーブルデータ生成

  function createTable(obj) {
var sort = initSortKey;
if (obj != undefined && obj.id != undefined) {
sort = obj.id;
}
// ソート項目判定
if (!isEnableSort(sort)) {
return false; // ソート項目でなければ処理終了
}
asc = !asc; // 昇順/降順切り替え
if (nowSortKey != sort) {
asc = false; // ソート項目変更時は降順
nowSortKey = sort;
}
// ヘッダー編集
editHeader();

var tbodyElm = document.getElementById("tbody_detail");
// tbody配下クリア
deleteTable(tbodyElm);
// データを絞込み
data = refineData(data);
// データをソート
data.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<data.length; i++) {
tbodyElm.appendChild(createTrElement(data[i]));
if (i == dispNum) {
break;
}
}
}

メイン処理です。大元は変わりませんが、冒頭のごちゃごちゃした部分を関数化しました。

また、最後に簡易的に表示件数を絞る判定を追加しています。

データ絞込み

  function refineData(data) {
data = refineDataAttr(data); // 属性
data = refineDataType(data); // タイプ
return data;
}

データの絞込み処理です。ソートの前に行います。

ここで処理を全て記載するつもりでしたが、ソースが複雑化、肥大化しそうだったので、絞込みの種類ごとに関数を更に細分化しています。

今後、コストXX以下とか、HP○○以上みたいな条件もここに追加するかもしれません。

データ絞込み(各条件)

  function refineDataAttr(data) {
if (refineAttr[0]) {
// 条件「全て」が選択されている場合、全データ表示
return data;
}
var result = new Array();
var cnt = 0;
for (var i=0; i<data.length; i++) {
if )((refineAttr[1] && (data[i]["AttrMain"] == '火' || data[i]["AttrSub"] == '火'))(
|| (refineAttr[2] && (data[i]["AttrMain"] == '水' || data[i]["AttrSub"] == '水'))
|| (refineAttr[3] && (data[i]["AttrMain"] == '木' || data[i]["AttrSub"] == '木'))
|| (refineAttr[4] && (data[i]["AttrMain"] == '光' || data[i]["AttrSub"] == '光'))
|| (refineAttr[5] && (data[i]["AttrMain"] == '闇' || data[i]["AttrSub"] == '闇'))) {
result[cnt] = data[i];
cnt++;
continue;
}
}
return result;
}
function refineDataType(data) {
if (refineType[0]) {
// 条件「全て」が選択されている場合、全データ表示
return data;
}
var result = new Array();
var cnt = 0;
for (var i=0; i<data.length; i++) {
if )((refineType[1] && (data[i]["Type1"] == 'ドラゴン' || data[i]["Type2"] == 'ドラゴン'))(
|| (refineType[2] && (data[i]["Type1"] == 'バランス' || data[i]["Type2"] == 'バランス'))
|| (refineType[3] && (data[i]["Type1"] == '体力' || data[i]["Type2"] == '体力'))
|| (refineType[4] && (data[i]["Type1"] == '回復' || data[i]["Type2"] == '回復'))
|| (refineType[5] && (data[i]["Type1"] == '攻撃' || data[i]["Type2"] == '攻撃'))
|| (refineType[6] && (data[i]["Type1"] == '神' || data[i]["Type2"] == '神'))
|| (refineType[7] && (data[i]["Type1"] == '悪魔' || data[i]["Type2"] == '悪魔'))) {
result[cnt] = data[i];
cnt++;
continue;
}
}
return result;
}

細分化した絞込みの各条件関数です。

今回は属性とタイプの絞込み機能を追加しました。

内容的には、「全て」が選択されている場合は絞り込みをせずに返却し、それ以外ならOR条件でいずれかに引っかかったら表示用返却データに改めて格納しています。

絞込み条件設定

  function editCondition(obj) {
var refineArray = obj.id.split('_');
this[refineArray[0]][refineArray[1]] = !this[refineArray[0]][refineArray[1]];
if (refineArray[1] == '0') {
// 条件「全て」選択時
if (this[refineArray[0]][refineArray[1]]) {
resetCondition(refineArray[0]);
obj.className += " cnBtn_on";
for (var i=1; i<this[refineArray[0]].length; i++) {
document.getElementById(refineArray[0] + "_" + i).className = "cnBtn";
}
} else {
return false;
}
} else if (this[refineArray[0]][refineArray[1]]) {
obj.className += " cnBtn_on";
document.getElementById(refineArray[0] + "_0").className = "cnBtn";
this[refineArray[0]][0] = false;
} else {
obj.className = "cnBtn";
this[refineArray[0]][0] = true;
document.getElementById(refineArray[0] + "_0").className += " cnBtn_on";
for (var i=1; i<this[refineArray[0]].length; i++) {
if (this[refineArray[0]][i]) {
this[refineArray[0]][0] = false;
document.getElementById(refineArray[0] + "_0").className = "cnBtn";
break;
}
}
}
asc = !asc;
var sortObj = new Object();
sortObj.id = nowSortKey;
initData(sortObj);
}

絞込みのボタンを押したときの処理です。どのボタンを押してもこの関数が呼ばれます。

各ボタンには"refineHoge_X"という命名規則でid属性を付与してあります。"Hoge"には絞込みの種類、"X"には配列番号がそれぞれ対応しています。

まだソースが汚いですが、面倒なのでここは多分もう手を入れません。

今後の改良点

  • 絞込み条件を追加する
  • JSONデータの取り扱い方を考える(絞込み、ソートのたびに通信が発生する)

多分、次回はまったく別のテーマでスクリプトを組んでいそうな気がします。