SlickGrid で遊んでみる
SlickGrid で遊んでみる:
Excelみたいなのを作るのに便利なJavaScriptライブラリ
https://github.com/mleibman/SlickGrid/wiki
詳細はこの辺
https://qiita.com/icoxfog417/items/98e34c0555991033afec
Excelで設定データを入力していたら1000超えたあたりで重い上に面倒になったのでいっそのこと自作してしまおうと思った。
GUIベースだとMacとWindowsを行き来する今の環境だと面倒なのでブラウザベースにした。
一から書くと大変なのであちこちからサンプルを参考(コピペしてカスタマイズ)して使っていますが、どこから持ってきたのか覚えて居ない……。
完成したのがこんなコード(cssはフォームの表示を整える為にあります)
※元のライセンスがMITライセンスなのでMITライセンスになると思います。
コードがあまりに汚い。
フォームも起動させようとするやつ
Excelより軽くて便利
SlickGridとは
Excelみたいなのを作るのに便利なJavaScriptライブラリhttps://github.com/mleibman/SlickGrid/wiki
詳細はこの辺
https://qiita.com/icoxfog417/items/98e34c0555991033afec
何をしたかったのか
Excelで設定データを入力していたら1000超えたあたりで重い上に面倒になったのでいっそのこと自作してしまおうと思った。GUIベースだとMacとWindowsを行き来する今の環境だと面倒なのでブラウザベースにした。
仕様
- データの更新を検出したらデータベースを更新(データをJSONにしてAPIを叩く)
- データの更新は編集行が変わった時だけに行う(更新頻度が高いと負荷がかかるため)
- データの更新はセルを選択すれば可能
- UpdateかInsertかはサーバー側で処理する(update APIで行う)
- データベースはsqlite3を選択(検索APIを考慮するとcsvよりデータベースを使った方が良い)
- エラーメッセージはコンソールログに出し、アラートではださない
- 強制更新ボタンは今回は付けていない(関数呼び出すだけなので実装自体は楽)
- 絞り込みが可能になっている(search APIで行う)
- サーバーにはPythonの簡易サーバー(CGIHTTPServer)を使用(クライアントPC1台でも完結させたいため)
- 見栄えはcssで後からどうにかする
- データもコードもクラウドストレージと同期させる(データの管理を楽にするため)
実装
SAMPLEを利用する
一から書くと大変なのであちこちからサンプルを参考(コピペしてカスタマイズ)して使っていますが、どこから持ってきたのか覚えて居ない……。完成したのがこんなコード(cssはフォームの表示を整える為にあります)
※元のライセンスがMITライセンスなのでMITライセンスになると思います。
HTML
wordedit.html
<!DOCTYPE HTML> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <title>編集</title> <link rel="stylesheet" href="./lib/slickgird/slick.grid.css" type="text/css"/> <link rel="stylesheet" href="./lib/slickgird/css/smoothness/jquery-ui-1.8.16.custom.css" type="text/css"/> <link rel="stylesheet" href="./serch.css" type="text/css"> <!--- <link rel="stylesheet" href="examples.css" type="text/css"/> --> <!-- form SlickGrid example Spread Sheet --> <style> .slick-cell.copied { background: blue; background: rgba(0, 0, 255, 0.2); } </style> </head> <body> <div id = "header"> <div class = "header_text">絞り込み:<input type="text" id="word" class="textbox" size="10" onkeydown="CreateSpreadSheet(document.getElementById('word').value);"></div> </div> <div style="position:relative 1;top: 200px;"> <div style="width:100%;"> <div id="myGrid" style="height:800px;"></div> </div> <script src="./lib/slickgird/lib/firebugx.js"></script> <script src="./lib/slickgird/lib/jquery-1.7.min.js"></script> <script src="./lib/slickgird/lib/jquery-ui-1.8.16.custom.min.js"></script> <script src="./lib/slickgird/lib/jquery.event.drag-2.2.js"></script> <script src="./lib/slickgird/slick.core.js"></script> <script src="./lib/slickgird/plugins/slick.autotooltips.js"></script> <script src="./lib/slickgird/plugins/slick.cellrangedecorator.js"></script> <script src="./lib/slickgird/plugins/slick.cellrangeselector.js"></script> <script src="./lib/slickgird/plugins/slick.cellcopymanager.js"></script> <script src="./lib/slickgird/plugins/slick.cellselectionmodel.js"></script> <script src="./lib/slickgird/slick.editors.js"></script> <script src="./lib/slickgird/slick.grid.js"></script> <script> //検索APIの指定 const searchapi = './api/v1/search'; //更新APIの指定 const updateapi = './api/v1/update'; var grid; var data = []; var options = { editable: true, enableAddRow: true, enableCellNavigation: true, asyncEditorLoading: true, autoEdit: true }; var columns = [ { id: "selector", name: "", field: "num", width: 60 } ]; fieldnames = ['単語','読み','コメント']; //フィールド名を指定 fieldids = ['word','read','comment']; //フィールドIDを指定 //上二つの数は一致すること for (var i = 0; i < fieldnames.length; i++) { columns.push({ id: fieldids[i], name: fieldnames[i], field: fieldids[i], width: 120, editor: FormulaEditor }); } /*** * A proof-of-concept cell editor with Excel-like range selection and insertion. */ function FormulaEditor(args) { var _self = this; var _editor = new Slick.Editors.Text(args); var _selector; $.extend(this, _editor); function init() { // register a plugin to select a range and append it to the textbox // since events are fired in reverse order (most recently added are executed first), // this will override other plugins like moverows or selection model and will // not require the grid to not be in the edit mode _selector = new Slick.CellRangeSelector(); _selector.onCellRangeSelected.subscribe(_self.handleCellRangeSelected); args.grid.registerPlugin(_selector); } this.destroy = function () { _selector.onCellRangeSelected.unsubscribe(_self.handleCellRangeSelected); grid.unregisterPlugin(_selector); _editor.destroy(); }; this.handleCellRangeSelected = function (e, args) { _editor.setValue( _editor.getValue() + grid.getColumns()[args.range.fromCell].name + args.range.fromRow + ":" + grid.getColumns()[args.range.toCell].name + args.range.toRow ); }; init(); } $(function () { CreateSpreadSheet('all'); }); function CreateSpreadSheet(opt){ let datacount = 0; if(opt == 'all' || opt == '') { opt = 'all=1'; } else { opt = 'word=' + opt; } $.getJSON(searchapi,opt,function(data, status, xhr) { datacount = data.length; for( let i = 1 ;i <= data.length; i++){ let d = (data[i-1]); d["num"] = i; } grid = new Slick.Grid("#myGrid", data, columns, options); grid.setSelectionModel(new Slick.CellSelectionModel()); grid.registerPlugin(new Slick.AutoTooltips()); // set keyboard focus on the grid grid.getCanvasNode().focus(); var copyManager = new Slick.CellCopyManager(); grid.registerPlugin(copyManager); copyManager.onPasteCells.subscribe(function (e, args) { if (args.from.length !== 1 || args.to.length !== 1) { throw "This implementation only supports single range copy and paste operations"; } var from = args.from[0]; var to = args.to[0]; var val; for (var i = 0; i <= from.toRow - from.fromRow; i++) { for (var j = 0; j <= from.toCell - from.fromCell; j++) { if (i <= to.toRow - to.fromRow && j <= to.toCell - to.fromCell) { val = data[from.fromRow + i][columns[from.fromCell + j].field]; data[to.fromRow + i][columns[to.fromCell + j].field] = val; grid.invalidateRow(to.fromRow + i); } } } grid.render(); }); grid.onAddNewRow.subscribe(function (e, args) { var item = args.item; var column = args.column; grid.invalidateRow(data.length); data.push(item); grid.updateRowCount(); grid.render(); }); let beforeRow = -1; let beforeRowdata = {}; grid.onSelectedRowsChanged.subscribe(function (e, args) { rownum = args.rows[0]; if(beforeRow >= 0 && beforeRow != rownum){ //編集行が変わったチェック console.log(beforeRow); rowdata = grid.getDataItem(beforeRow); let a = JSON.stringify(beforeRowdata); // 編集前データ let b = JSON.stringify(rowdata); // 編集後データ if(a !== b ){ // 変更があるかチェック console.log('edited!'); // 更新APIを呼び出す //サンプルではJOINEDキーを使っているので更新前のデータを二つ送っている rowdata["oldword"] = beforeRowdata["word"]; rowdata["oldread"] = beforeRowdata["read"]; let params = JSON.stringify(rowdata); console.log(params); //Application/JSONで送りたかったのですがPythonのCGIServerだと上手く取得できる方法が分からなかったのでのでx-www-form-urlencodedのdata='jsondata'でPOSTしています。 $.ajax({ type: 'POST', // method = "POST" url: updateapi, // POST送信先のURL data: "data=" + params, // JSONデータ本体 contentType: 'application/x-www-form-urlencoded', // リクエストの Content-Type dataType: 'text', success: function(json_data) { // 200 OK時 console.log(json_data); }, error: function(json_data) { console.log(json_data); console.log('server error'); }, complete: function() { // 成功・失敗に関わらず通信が終了した際の処理 } }); } $.extend(true,beforeRowdata,grid.getDataItem(rownum)); } else if(beforeRow < 0){ $.extend(true,beforeRowdata,grid.getDataItem(rownum)); } beforeRow = rownum; }); }); } </script> </body> </html>
Update APIとSearch APIを作成
コードがあまりに汚い。
テーブルスキーマ
drop table WordDictionary; create table WordDictionary( WORD text not null, READ text not null, comment text, UNIQUE (WORD,READ) );
Search API(JSONで検索結果を返す)
search
#!/usr/local/bin/python3 import urllib import cgitb import sqlite3 import json from contextlib import closing import sys import configparser import os import os.path as path #config.iniにはデータベースファイルの位置が入っている #実行環境によって相対パスでは取得できないのでsearch APIのフォルダの場所を自動取得 #config.ini #[setting] #database=/path/too inipath = path.join(path.dirname(sys.argv[0]),'config.ini') inifile = configparser.ConfigParser() inifile.read( inipath , 'UTF-8') dbname = inifile.get('settings', 'database') #cgitb.enable() def query4Yomi(conn,yomi): cur = conn.cursor() yomi = '%' + yomi + '%' return cur.execute('select * from WordDictionary where READ like ?', (yomi,)) def query4Word(conn,word): cur = conn.cursor() return cur.execute('select * from WordDictionary where WORD = ?', (word,)) def queryall(conn): cur = conn.cursor() return cur.execute('select * from WordDictionary order by READ') def query4Any(conn,word): cur = conn.cursor() word = '%' + word + '%' return cur.execute('select * from WordDictionary where READ like ? or WORD like ? or comment like ? order by READ', (word,word,word,)) with closing(sqlite3.connect(dbname)) as conn: if 'QUERY_STRING' in os.environ: query = urllib.parse.parse_qs(os.environ['QUERY_STRING']) else: query = {} if ( 'word' in query ): # results = query4Yomi(conn,query['word'][0]) results = query4Any(conn,query['word'][0]) elif ( 'all' in query ): results = queryall(conn) else: results = {} print ("Content-Type: application/json") print ("") print ('[') line = 0 for row in results: if (line !=0): print (',') print ('{') print ('"word": "' + row[0] + '"') print (',"read": "' + row[1] + '"') print (',"comment": "' + row[2] + '"') print ('}') line += 1 print (']')
Update API(data='JSONDATA'でJSONDATAの内容をデータベースにアップ
update
#!/usr/local/bin/python3 import cgi import cgitb import sqlite3 import sys import json import configparser import os import os.path as path #config.iniにはデータベースファイルの位置が入っている #実行環境によって相対パスでは取得できないのでupdate APIのフォルダの場所を自動取得 #config.ini #[setting] #database=/path/too inipath = path.join(path.dirname(sys.argv[0]),'config.ini') inifile = configparser.ConfigParser() inifile.read( inipath , 'UTF-8') dbname = inifile.get('settings', 'database') from contextlib import closing def update(conn,param): cur = conn.cursor() cur.execute('update WordDictionary ' + 'SET WORD = ?, READ = ?, comment = ? WHERE WORD = ? and READ = ?', (param['word'],param['read'],param['comment'],param['oldword'],param['oldread'])) conn.commit() def query(conn,word,read): cur = conn.cursor() cur.execute('select WORD,READ from WordDictionary where WORD = ? and READ = ?', (word,read)) s = cur.fetchone() try: if (str(s[0]) == word and str(s[1]) == read): return (1,s) else: return (0,None) except: pass return (0,None) def insert(conn,param): cur = conn.cursor() p ={'comment': ''} for key in p.keys(): if(key in param): p[key] = param[key] cur.execute('insert INTO WordDictionary ' + 'VALUES (?, ?, ?)', (param['word'],param['read'],p['comment']) conn.commit() with closing(sqlite3.connect(dbname)) as conn: print ("Content-Type: application/json") print ("") # if (os.environ['REQUEST_METHOD'] != "POST"): # print (str(os.environ['REQUEST_METHOD'])) form = cgi.FieldStorage() q = json.loads(form['data'].value) try: (r,s0) = query(conn,q['oldword'],q['oldread']) if(r != 1) : insert(conn,q) m = 'insert' else: update(conn,q) m = 'update' (r,s1) = query(conn,q['word'],q['read']) results = {"result" : "200" , "method" : m} print (results) except: import traceback as tr s = tr.format_exc() results = {"result" : "403" ,"trace" : s} print (results)
簡易サーバー
cgiserver
#!/usr/local/bin/python3 # -*- coding: utf-8 -*- import http.server import threading import webbrowser import time handler = http.server.CGIHTTPRequestHandler handler.cgi_directories = ['/cgi-bin','/api'] http.server.test(HandlerClass=handler)
cgiserver.py
#!/usr/local/bin/python3 # -*- coding: utf-8 -*- import http.server import threading import webbrowser import time url = 'http://localhost:8000/wordedit.html' def openBrowser(url): time.sleep(1) print ("OPEN URL {}".format(url)) webbrowser.open(url) #1秒待ってからブラウザを起動 try: if __name__ == "__main__": thread = threading.Thread(target=openBrowser,args=(url,)) thread.start() except: print ("Error: unable to start thread") handler = http.server.CGIHTTPRequestHandler handler.cgi_directories = ['/cgi-bin','/api'] http.server.test(HandlerClass=handler)
結果
Excelより軽くて便利
課題
- 必要なJSがどれか全く分かっていない
- テーブルのメタデータからフィールド名を自動生成&自動処理可能にする(汎用化)
- 動作環境が変わった時に設定ファイルをいじらなくても動くようにする
- JavaScriptのコードをHTMLから分離
- 新規追加に少しトリッキーな操作を要求する(データベースでエラーが出た場合の処理が甘い)
- 一括更新
コメント
コメントを投稿