【JavaScript】テーブルを編集可能にするスクリプトを書いてみた。
こんにちは。
最近は便利なものが多くて
自作した編集できるテーブルなんか需要がないだろうと思いつつ
昔作ったものが出てきたので晒してみる。
あんまり覚えていないので質問等には答えられないかもしれません。笑
スポンサーリンク
どんなものか
Excelっぽく操作できるようにしました。
- 矢印、Tab、Enterキーでセルの移動ができます。
↑→↓← / Tabで→、Shift+Tabで← / Entarで↓、Shift+Enterで↑ - 矢印、Enterキーの場合はテーブル内を循環しません。
Tabキーの場合は右端まできたら次の行の最初に、左端まできたら前の行の最後に移動します。 - 編集モード切替
F2キーを押すと編集モードを切り替えることができます。
※ 編集モード中は枠線の色が変わります
○編集モード中の動作
・矢印キーでテキストボックス内の文字列をカーソル移動します。
※ドロップダウンの場合は選択が変更されます。
・Tabキーで左右に移動することができます。
・F2またはEnterキーで編集モードを終了します。 - 入力可能列の切替
表のヘッダーにあるチェックボックスにチェックがついている列は入力可能となります。
チェックを外すと、その列は選択不可となり、キーで移動した場合も飛ばされます。 - その他
○ドロップダウンリストの選択変更方法について
・マウスでクリックすると、ドロップダウンリストが開かれます。
・セルを選択中に Alt + 矢印キー で編集モードに切り替わります。
○入力テキストボックスの最大入力桁数は 20桁にしてあります。
サンプルGIF
ソースコード
画面
JavaScript、cssを読み込んで、適当にテーブルタグを配置。
sample.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <style type="text/css"> <!-- @import url(./css/sample.css); --> </style> <script type="text/javascript" src="./js/common.js"></script> <script type="text/javascript" src="./js/sample.js"></script> </head> <body> <div> 【編集可能テーブル】 </div> <table id="editableTable" cellspacing="0"> <tr> <th>入力1<input type="checkbox" value="0" name="editCheck" checked/></th> <th>入力2<input type="checkbox" value="1" name="editCheck" checked/></th> <th>入力3<input type="checkbox" value="2" name="editCheck" checked/></th> <th>入力4<input type="checkbox" value="3" name="editCheck" checked/></th> <th>入力5<input type="checkbox" value="4" name="editCheck" checked/></th> <th>選択1<input type="checkbox" value="5" name="editCheck" checked/></th> </tr> <tr> <td>1-1</td> <td>1-2</td> <td>1-3</td> <td>1-4</td> <td>1-5</td> <td>区分1</td> </tr> <tr> <td>2-1</td> <td>2-2</td> <td>2-3</td> <td>2-4</td> <td>2-5</td> <td>区分2</td> </tr> <tr> <td>3-1</td> <td>3-2</td> <td>3-3</td> <td>3-4</td> <td>3-5</td> <td>区分3</td> </tr> <tr> <td>4-1</td> <td>4-2</td> <td>4-3</td> <td>4-4</td> <td>4-5</td> <td>区分4</td> </tr> <tr> <td>5-1</td> <td>5-2</td> <td>5-3</td> <td>5-4</td> <td>5-5</td> <td>区分5</td> </tr> </table> </body> </html>
Javascript
IDからDOMを取得したり、イベント追加メソッドを定義したりしてます。
今なら JQuery なんかを読み込めば同じことがサクッとできるんじゃないかと思います。
common.js
/** * 指定イベントを指定エレメントに追加する * * @param element * @param type * @param event * @return */ var addListener = (function() { if (window.addEventListener) { return function(el, tp, fn) { el.addEventListener(tp, fn, false); }; } else if (window.attachEvent) { return function(el, tp, fn) { el.attachEvent('on'+tp, function() { return fn.call(el, window.event); }); }; } else { return function(el, tp, fn) { element['on'+tp] = fn; }; } })(); /** * 指定イベントを指定エレメントから削除する * * @param element * @param type * @param event * @return */ function removeEvent(element, type, event){ if(element.removeEventListener){ element.removeEventListener(type, event, false); } else if(element.detachEvent){ element.detachEvent("on" + type, event); } else{ element["on" + type] = null; } } /** * IDからオブジェクトを取得する. * * @param id * @return */ function $i(id) { if(document.getElementById) return document.getElementById(id); // IE4 if(document.all) return document.all(id); // NN4 if(document.layers){ var s=''; for(var i=1; i<arguments.length; i++) s+='document.layers.'+arguments[i]+'.'; return eval(s+'document.layers.'+id); } return null; } /** * * オブジェクト同士の位置、サイズを合わせる * */ function syncPositionAndSize(target, base, flg) { /* ポジション */ syncPosition(target, base, 0, 0); /* サイズ */ syncSize(target, base, flg); target.style.display = 'block'; } /** * オブジェクト同士の位置を合わせる * */ function syncPosition(target, base, leftPoint, topPoint) { var pos = getElementPosition(base); if (leftPoint != null && leftPoint != 0) { pos.left += base.offsetWidth + leftPoint; } if (topPoint != null && topPoint != 0) { pos.top += base.offsetHeight + topPoint; } // 画面からはみ出さないよう、マイナスの場合は0に target.style.left = pos.left < 0 ? 0 + "px" : (pos.left -1) + "px"; target.style.top = pos.top < 0 ? 0 + "px" : (pos.top -1) + "px"; } /** * * オブジェクト同士のサイズを合わせる * */ function syncSize(target, base, flg) { var offset = 0; if (flg == 1) { offset = 3; } target.style.width = (base.offsetWidth - offset) + "px"; target.style.height = (base.offsetHeight - offset) + "px"; target.style.lineHeight = target.style.height; } /** * エレメントの座標を取得 * */ function getElementPosition(element) { var offsetTop = 0; var offsetLeft = 0; try { while (element) { if (element == undefined || element == null) { return { top : offsetTop, left : offsetLeft }; } offsetTop += element.offsetTop; offsetLeft += element.offsetLeft; element = element.offsetParent; } } catch (e) { // なんだかエラーになることがあるので return { top : offsetTop, left : offsetLeft } } return { top : offsetTop, left : offsetLeft } }
画面に設置したテーブルを編集可能にします。
sample.js
var INPUT_MAX_LEN = 20; var d = document; var isIE = "\v" == "v"; /** * 列が編集可能かどうかの設定 * デフォルトは全て編集可 * */ var editableColmun = { 0 : true , 1 : true , 2 : true , 3 : true , 4 : true , 5 : true }; /** * 列タイプの設定. * text : テキストボックス * list : ドロップダウンリスト * */ var colmunType = { 0 : "text" , 1 : "text" , 2 : "text" , 3 : "text" , 4 : "text" , 5 : "list" }; /** * 選択ボックスに表示するデータ. * */ var dataList = [ {value : "0001", text : "区分1"} , {value : "0002", text : "区分2"} , {value : "0003", text : "区分3"} , {value : "0004", text : "区分4"} , {value : "0005", text : "区分5"} ]; /** * テーブルマネージャ. * */ var TableManager = function() { this.table; this.tableRows; this.tableCols; this.matrix = new Array(); this.selectedCell; this.editMode = false; }; TableManager.prototype = { // テーブルマネージャのセットアップ setUp: function() { // テーブルの設定 this.table = $i("editableTable"); this.tableRows = this.table.rows.length -1; this.tableCols = this.table.rows.item(0).cells.length; addListener(this.table, "mousedown", function(e) { // クリック時の要素を取得し td でなければ処理中止 var cell = e.srcElement? e.srcElement: e.target; if (cell.tagName != "TD"){ return; } // 要素がテーブルに属している場合 if (manager.isChildElement(cell)) { // クリックイベントを上位伝播させない if (manager.selectedCell.id != cell.id) { if (isIE) { event.returnValue = false; event.cancelBubble = true; } else { e.preventDefault(); e.stopPropagation(); } } // 選択処理を実施 // その後テキストボックスだった場合に選択状態にする var item = manager.selected(cell); if (item) { if (item.nodeName == "INPUT") { setTimeout( function () { item.select(); } , 1 ); } } } }); // セルの設定、マトリクス表を作成する // タグ名で取得しているのを改良する必要がある var list = d.getElementsByTagName("td"); var r = 0, c = 0; for (var i = 0, len = list.length; i < len; i++) { if (!this.matrix[r]) this.matrix[r] = new Array(); list[i].setAttribute("id", "cell[" + r + "][" + c + "]"); this.matrix[r][c] = new TableCell(list[i].id, r, c); if (c == (this.tableCols -1)){ c = 0; r++; } else { c++; } } }, // セル選択処理 selected: function (cell) { // 選択クリア if (this.selectedCell && this.selectedCell != "") { this.clearSelected(); } var mtrx = this.getMatrixByElement(cell); // 入力項目を入れる div を生成 // ※ TDに追加すると幅の調整が困難なため var div = d.createElement("div"); div.className = "editField"; div.setAttribute("id", "editField"); var input; // 編集可能なセルだった場合の設定 if (mtrx.isEditable) { // 入力コントロール生成 input = this.createElement(mtrx); // 各種属性の追加 input.setAttribute("name", cell.id.replace(/cell/, "input")); input.setAttribute("id", cell.id.replace(/cell/, "input")); input.className = "nomal"; // 要素をページに追加 div.appendChild(input); d.body.appendChild(div); // 入力欄制御 if (input.nodeName == "INPUT") { input.focus(); input.select(); syncPositionAndSize(div, cell, 1); syncSize(input, div, 1); } else if (input.nodeName == "SELECT") { syncPositionAndSize(div, cell, 0); syncSize(input, div, 0); } this.selectedCell = mtrx; } return input; }, // 編集モードの切替を実施 changeEditMode: function (isEdit) { this.editMode = isEdit; var input = $i(manager.selectedCell.id.replace(/cell/, "input")); if (input) { input.className = isEdit ? "edit" : "nomal"; } }, // セルの選択をクリア clearSelected: function () { for (var i = 0, il = this.matrix.length; i < il; i++) { for (var j = 0, jl = this.matrix[i].length; j < jl; j++) { var input = $i(this.matrix[i][j].id.replace(/cell/, "input")); if (input) { if (input.nodeName == "INPUT") { $i(this.matrix[i][j].id).innerHTML = input.value; } else if (input.nodeName == "SELECT") { $i(this.matrix[i][j].id).innerHTML = input.options[input.options.selectedIndex].innerHTML; } d.body.removeChild($i("editField")); manager.selectedCell = ""; } } } }, // テーブルの子要素かどうかを返します isChildElement: function (element) { // 親要素が入力項目格納用の div であれば true if (element.offsetParent != null && element.offsetParent.id == "editField") { return true; } // そうでなければ親要素を順番に調べていく while (element) { if (element.id == this.table.id) { return true; } element = element.offsetParent; } return false; }, // 指定された座標のセルに移動できるかどうかを返します isMovable: function(r, c) { return (manager.matrix[r] && manager.matrix[r][c]); }, // 指定された座標に選択を移動します move: function (r, c) { if (this.isMovable(r, c)) { manager.selected($i(manager.matrix[r][c].id)); return true; } return false; }, // 現在の座標から上に移動します moveUp: function () { var r = new Number(this.selectedCell.rowIdx) - 1; var c = new Number(this.selectedCell.colIdx); return this.move(r, c); }, // 現在の座標から右に移動します moveRight: function () { var r = new Number(this.selectedCell.rowIdx); var c = new Number(this.selectedCell.colIdx) + 1; while (this.isMovable(r, c) && !manager.matrix[r][c].isEditable) { c++; } return this.move(r, c); }, // 現在の座標から右に移動します(右端であれば次の行の最初に移動) moveRightExt: function() { if (!this.moveRight()) { var r = new Number(this.selectedCell.rowIdx) + 1; var c = new Number(0); while (this.isMovable(r, c) && !manager.matrix[r][c].isEditable) { c++; } return this.move(r, c); } }, // 現在の座標から下に移動します moveDown: function () { var r = new Number(this.selectedCell.rowIdx) + 1; var c = new Number(this.selectedCell.colIdx); return this.move(r, c); }, // 現在の座標から左に移動します moveLeft: function () { var r = new Number(this.selectedCell.rowIdx); var c = new Number(this.selectedCell.colIdx) - 1; while (this.isMovable(r, c) && !manager.matrix[r][c].isEditable) { c--; } return this.move(r, c); }, // 現在の座標から左に移動します(左端であれば前の行の最後に移動) moveLeftExt: function () { if (!this.moveLeft()) { var r = new Number(this.selectedCell.rowIdx) - 1; var c = new Number((this.tableCols -1)); while (this.isMovable(r, c) && !manager.matrix[r][c].isEditable) { c--; } return this.move(r, c); } }, // 要素からマトリクス表の項目を取得します getMatrixByElement: function (cell) { for (var i = 0, il = this.matrix.length; i < il; i++) { for (var j = 0, jl = this.matrix[i].length; j < jl; j++) { if (cell.id == this.matrix[i][j].id) { return this.matrix[i][j]; } } } }, // セルの入力項目を作成します // スクリプトファイルの最初に定義した colmunType を利用 createElement: function (mtrx) { var element, type = colmunType[mtrx.colIdx]; var value = $i(mtrx.id).innerHTML; if (type == "text") { element = d.createElement("input"); element.value = value; element.setAttribute("maxlength", INPUT_MAX_LEN); } else if (type == "list") { element = d.createElement("select"); for (var i = 0, len = dataList.length; i < len; i++) { element.options[i] = new Option(dataList[i].text, dataList[i].value); if (i == 0) { element.options.selectedIndex = 0; } else { if (dataList[i].text == value) { element.options.selectedIndex = i; } } } } return element; } }; /** * テーブルセル. * */ var TableCell = function(id , rowIdx, colIdx) { this.id = id; this.rowIdx = rowIdx; this.colIdx = colIdx; this.selected = false; this.type; this.isEditable = editableColmun[colIdx]; this.setUp(); }; TableCell.prototype = { // テーブルセルのセットアップ setUp: function () { var cell = $i(this.id); // イベント追加 addListener(cell, "mouseout", function(e) { if (cell.className != "selected") { cell.className = ""; } }); addListener(cell, "mouseover", function(e) { if (cell.className != "selected") { cell.className = "over"; } }); } }; // マネージャインスタンス生成 var manager; /** * ページ初期化. * */ function initPage() { // イベントの追加 addListener(d, "keydown", keyEventHandler); addListener(d, "mousedown", mouseDownEventHandler); // ページ設定 setUpPage(); } /** * ページ設定 * 初期化時以外にも呼び出されるため切り分け. * */ function setUpPage() { // 編集可能チェックボックスの設定 var checkList = d.getElementsByName("editCheck"); for (var i = 0, len = checkList.length; i < len; i++) { editableColmun[checkList[i].value] = checkList[i].checked; } // マネージャの初期化 manager = new TableManager(); manager.setUp(); // 初期選択セルの設定 var flg = false; for (var i = 0, ilen = manager.tableRows; i < ilen; i++) { for (var j = 0, jlen = manager.tableCols; j < jlen; j++) { if (manager.matrix[i][j].isEditable) { manager.selected($i("cell[" + i + "][" + j + "]")); flg = true; break; } } if (flg) break; } } /** * キーイベントマネージャ. * */ var KeyEventManager = function () { this.keyCode; this.isShift; this.isCtrl; this.isAlt; this.item; this.itemName; this.keyCodeList = { 9 : 'tab' , 13 : 'enter' , 37 : 'left' , 38 : 'up' , 39 : 'right' , 40 : 'down' , 113 : 'f2' }; this.isContinue = false; }; KeyEventManager.prototype = { // キーイベントマネージャのセットアップ setUp : function (e) { // キーコード、補助キーの設定 if (isIE) { this.keyCode = e.keyCode; this.isShift = e.shiftKey; this.isCtrl = e.ctrlKey; this.isAlt = e.altKey; } else { this.keyCode = e.which; this.isCtrl = typeof e.modifiers == "undefined" ? e.ctrlKey : e.modifiers & Event.CONTROL_MASK; this.isShift = typeof e.modifiers == "undefined" ? e.shiftKey : e.modifiers & Event.SHIFT_MASK; this.isAlt = typeof e.modifiers == "undefined" ? e.altKey : e.modifiers & Event.ALT_MASK; } // キーイベント発生時の選択中項目を設定 this.item = $i(manager.selectedCell.id.replace(/cell/, "input")); if (this.item) { this.itemName = this.item.nodeName; } else { this.itemName = "NONE"; } // 処理を続行するかどうかを設定 this.isContinue = (this.keyCodeList[this.keyCode] != undefined); return this; }, // 押されたキーが Tab かどうかを返します. isTab : function () { return this.keyCodeList[this.keyCode] == "tab"; }, // 押されたキーが Shift + Tab かどうかを返します. isShiftTab : function () { return (this.isShift && this.isTab()); }, // 押されたキーが Enter かどうかを返します. isEnter : function () { return this.keyCodeList[this.keyCode] == "enter"; }, // 押されたキーが Shift + Enter かどうかを返します isShiftEnter : function () { return (this.isShift && this.isEnter()) }, // 押されたキーが 矢印キーの何れかかどうかを返します isArrow : function () { var value = this.keyCodeList[this.keyCode]; if (value == "left" || value == "up" || value == "right" || value == "down") { return true; } return false; }, // 押されたキーが f2 かどうかを返します isChangeEdit : function () { return this.keyCodeList[this.keyCode] == "f2"; }, // 上位伝播(キーダウンイベント自体を処理)するかどうかの設定 setEventPropagation : function (e, editMode) { if (!editMode) { if (this.isTab() || this.isArrow()) { if (isIE) { event.returnValue = false; event.cancelBubble = true; } else { e.preventDefault(); e.stopPropagation(); } } } else if (this.isAlt) { if (this.isTab() || this.isArrow()) { if (isIE) { event.returnValue = false; event.cancelBubble = true; } else { e.preventDefault(); e.stopPropagation(); } } } } }; /** * キーイベントハンドラー. * */ function keyEventHandler(e) { // セルが選択中かどうか判定 if (manager.selectedCell == null || manager.selectedCell == "") { return true; } // マネージャ生成 var km = new KeyEventManager().setUp(e); // 処理対象でない場合中止 if (!km.isContinue) return; // F2キーによる編集モード切替 if (km.isChangeEdit()) { // 編集モード設定 manager.changeEditMode(!manager.editMode); // ドロップダウンだった場合はフォーカスする if (km.itemName == "SELECT") { km.item.focus(); } } // キーを判定して処理を実施 // 選択中セルから押下されたキーの方向へ選択を移動 if (km.isShiftTab()) { // Shift + Tab manager.editMode = false; manager.moveLeftExt(); } else if (km.isShiftEnter()) { // Shift + Enter if (manager.editMode) { manager.changeEditMode(false); } else { manager.moveUp(); } } else if ((km.isAlt && km.itemName == "SELECT") && (km.isArrow())) { // SELECTリスト上で Alt + 矢印キー manager.changeEditMode(true); km.item.focus(); } else if (km.isTab()) { // Tabキー manager.editMode = false; manager.moveRightExt(); } else if (km.isEnter()) { // Enterキー if (manager.editMode) { manager.changeEditMode(false); } else { manager.moveDown(); } } else if (!manager.editMode) { // 各種矢印キー if (km.keyCode == "37") { manager.moveLeft(); } else if (km.keyCode == "38") { manager.moveUp(); } else if (km.keyCode == "39") { manager.moveRight(); } else if (km.keyCode == "40") { manager.moveDown(); } } // 上位伝播の判定及び設定 km.setEventPropagation(e, manager.editMode); } /** * マウスダウンハンドラー. * */ function mouseDownEventHandler(e) { // クリック時の要素を取得、テーブルに属していない場合選択クリア var element = e.srcElement? e.srcElement: e.target; if (!manager.isChildElement(element)) { manager.clearSelected(); } } /** * 編集可能列の切替. * */ function changeEditableCheck(element) { // 選択クリアしページ設定を実施 manager.clearSelected(); setUpPage(); }
スタイル
選択状態や、編集中などのスタイルを定義。
sample.css
body { font-family:"MS Pゴシック","MS PGothic",sans-serif; font-size: 12px; } table { border: 1px solid #ccc; border-collapse:collapse; } div.editField { position: absolute; width:0px; height:0px; } th { text-align:center; background-color:#404040; color:#ffffff; width: 100px; height: 20px; border: 1px solid #ccc; } td { padding-left:5px; width: 200px; height: 20px; border: 1px solid #ccc; } input { padding-top:2px; padding-left:2px; font-family:"MS Pゴシック","MS PGothic",sans-serif; } input.nomal { border-top: 3px solid #abc; border-right: 3px solid #abc; border-bottom: 3px solid #abc; border-left: 3px solid #abc; } input.edit { border-top: 3px solid #800040; border-right: 3px solid #800040; border-bottom: 3px solid #800040; border-left: 3px solid #800040; } select.nomal { border-top: 3px solid #abc; border-right: 3px solid #abc; border-bottom: 3px solid #abc; border-left: 3px solid #abc; } select.edit { border-top: 3px solid #800040; border-right: 3px solid #800040; border-bottom: 3px solid #800040; border-left: 3px solid #800040; } td.over { background-color: #FFDDDD; }
フォルダ構成
次のように保存して、sample.html を開けば表示できます。
sample.html
├─js
│ ├─common.js
│ └─sample.js
└─css
└─sample.css
ではでは。
ディスカッション
コメント一覧
役にたちそうです。ありがたく参考にさせていただきます。
F2キーによる編集モードというのは、効いているのでしょうか?
普通に編集モードとなっていますが・・。
いしみず様
いつもブログを見ていただきありがとうございます。
ご質問の件、編集モードにすると上下左右の矢印キーでセル内の文字列をカーソル移動できるようになります。
※編集モードでない場合は隣接するセルへ移動
よろしくお願いします。