初めてのGreasemonkey の作り方(Slimtimer Report to Ticket)
初めてGreasemonkey を作りました!javascript は全然できないので(初めてのjavascript 4章まで読んだくらい)、その辺のソースを組み合わせて作りました。
どんな機能?
Slimtimer というサービスがあります。簡単に言うと、タスクを登録して、タスクを行う前にボタンを押し、終わった後にまたボタンを押すと、そのタスクにかかった時間が計測できるというツール。(詳しくはSlimTimer (オンライン仕事タイマー) : ワークスタイル・メモ )
いつも、チケットに着手しているときはそのチケットをSlimtimer に登録しています。すると、「今週何やったの?」って言われた際に、Slimtimer のレポート画面を開くと一発で分かるので便利。
そこで、そのレポート画面から自分のTrac のチケットへリンクできると便利だなと思い作りました。
作り方
戦略としては次のような感じ。
- 1. レポート画面からタスクの名前の部分を抽出
- 2. タスクの名前に#xxxx(xは1桁以上の数字)が含まれているかのチェック
- 正規表現でいけそう
- 3. もし含まれていれば、タスク名の部分を以下のように置換
- 多分xxxx は正規表現でマッチさせた時の情報からとれそう
<a href="Trac のプロジェクトへのURL/ticket/xxxx" target="_blank">タスク名</a>
1. レポート画面からタスクの名前の部分を抽出
とりあえず、テーブルのセルのデータを得る方法を求めて、「javascript table」でぐぐる。以下のページを発見!
特定のセルの値を取り出すには,次のようにする。 <table id="hoge"> <tr><td>cell 1<td>cell 2 </table> function cell(id, x, y) { table = document.getElementById(id); row = table.rows.item(y); cell = row.cells.item(x); document.write("cell(" + x + ", " + y + ") is " + cell.firstChild.data); } cell('hoge', 1, 0);
ここからセルの値を得る方法がわかる。
- 1. table オブジェクトを得る
- getElementById でやっているけど、XPath で得ることもできるはず
- 2. table.rows で列のコレクションを得られる
- 3. row.cells でその列のセルを得られる
- 4. cell.firstChild.data でセルの値を得られる
ってことで、多分以下のようにやると値を得られる。デバッグは基本alert を使う。
var xpath ='xpath_to_table'; var table = xpath からオブジェクトを得る alert(table.rows[0].cells[0].firstChild.data)
ここまでをGreasemonkey にしてみる
どうせなら今回みたいにリンクに置換する系のGreasemonkey があると良いなと思い、「greasemonkey リンク」でぐぐる。どんぴしゃというのはなかったけど、以下のスクリプトが参考になりました。
ここから、次のことが分かった。
- XPaht からオブジェクトを取り出す方法
// evaluate の第一引数がxpath var target = document.evaluate("//b[@class='sans']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
- グリモンのメタデータ部分
- グリモンのロジック部分
- 以下の無名関数の中にロジックを書けばいい
(function(){ } )();
で、書いてみた!今回の対象ページのテーブルにはid がふられてなかったけど、tbody にid がふられていたのでそっちを使いました。table の代わりにtbody を使っても大丈夫だった。
// ==UserScript== // @name Slimtimer Report to Ticket // @namespace http://d.hatena.ne.jp/LukeSilvia/ // @description Create a link to ticket on the report by slimtimer. // @include http://slimtimer.com/tasks* // ==/UserScript== (function() { var xpath ='//*[@id="task-tbody"]'; var tbody = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; alert(tbody.rows[0].cells[0].firstChild.data) )();
おぉぉ〜!インストールして、「http://slimtimer.com/tasks」に行ったら、「ルーティン」っていうalert がでましたよ!(ルーティンは一番上タスクのタスク名)
以降は、インストールしたグリモンを編集して行きます。「ユーザースクリプトの管理」から編集できる。こうすると、編集した結果をすぐにブラウザで確かめられて便利。
2. タスクの名前に#xxxx(xは1桁以上の数字)が含まれているかのチェック
やることは以下のような感じ。
- 1. ループを使って、タスク名を得る
- 2. ループ内で正規表現を使って、タスク名に#xxxx が含まれるかチェック
js でもfor ループが使えるのは知っている。上記のグリモンの最後で「alert(rows.length)」 ってやったら列数が表示されたので、それを使えばよさそう。
後は、正規表現について調べる。「javascript 正規表現」でぐぐる。みっけた。
xx = "12:34:56".match(/(\d+):(\d+):(\d+)/); document.write(RegExp.$1 + "<BR>") // → 12 document.write(RegExp.$2 + "<BR>") // → 34 document.write(RegExp.$3 + "<BR>") // → 56
分かったこと。
ここまでをグリモンにしてみる
// ==UserScript== // @name Slimtimer Report to Ticket // @namespace http://d.hatena.ne.jp/LukeSilvia/ // @description Create a link to ticket on the report by slimtimer. // @include http://slimtimer.com/tasks* // ==/UserScript== (function() { var xpath ='//*[@id="task-tbody"]'; var tbody = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; var rows = tbody.rows; for(var i = 0; rows.length; i++) { task = rows[i].cells[0]; if(task.firstChild.data.match(/#(\d+)/)) { alert(RegExp.$1); } } } )();
ktkr!「1, 22」が順番にalert されましたー( ´艸`)
3. もし含まれていれば、タスク名の部分を以下のように置換
最後に、置換部分。ここでやることは1つ。
- 1. チケットのタスク名をa タグで囲む
このヒントはさっきも参考にしたグリモンgreasemonkeyでamazonにG-Tools個別商品ページへのリンクの中にありました。
var atg_link = document.createElement('a'); atg_link.setAttribute('href', 'http://www.umizoi.com/g-tools/foward_ecs4.php?view=detail®ion=jp&asin=' + asin); atg_link.setAttribute('title', 'Information on this commodity is acquired with G-Tool.'); atg_link.innerHTML = '<span style=\"margin-left: 1em; font-size:90%;\">G-Tools</span>'; header.parentNode.insertBefore(atg_link, header.nextSibling);
これでa タグの生成方法がわかったけど、多分insertBefore でリンクを追加している部分を変えないとだめそう。insertBefore は追加なので、今回の置換の目的には使えない。「javascript 置換 ノード」でぐぐる!
上記のHTMLでh1の要素をpにinnerHTMLを使わないで書き換えろという問題。 そう言えばdel.icio.usで「innerHTML遅いからW3C DOMを使おうよ」みたいなエントリ を見た覚えがあります。というわけで回答は以下。 var div = document.getElementById("elem1"); var children = div.childNodes; var h1 = children.getElementsByTagName("h1")[0]; var p = document.createElement("p"); var txt = document.createTextNode("This is replaced text."); child.appendChild(txt); div.replaceChild(p, h1); 置き換えるノードとその親ノードを習得して、 parentNode.replaceChild(置換するノード, 置換されるノード)で置換する。 中のテキストはテキストノードを作って、子ノードとして追加することで追加できる。
ここから分かること。
- replaceChild で要素の置換ができる
- アンカーテキストの部分はinnerHTML 使うより、テキストノードを作ってappendChild する方が今の仕様にあっている(XHTML 的な考え方か?)
できあがったグリモン
// ==UserScript== // @name Slimtimer Report to Ticket // @namespace http://d.hatena.ne.jp/LukeSilvia/ // @description Create a link to ticket on the report by slimtimer. // @include http://slimtimer.com/tasks* // ==/UserScript== (function() { var xpath ='//*[@id="task-tbody"]'; var tbody = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; var rows = tbody.rows; for(var i = 0; rows.length; i++) { task = rows[i].cells[0]; if(task.firstChild.data.match(/#(\d+)/)) { var task_link = document.createElement('a'); task_link.setAttribute("target", "_blank"); task_link.setAttribute( "href", "Trac のプロジェクトへのURL/ticket/" + RegExp.$1); task_link.appendChild(document.createTextNode(task.firstChild.data)); task.replaceChild(task_link, task.firstChild); } } } )();
動きましたよ〜(⊃д⊂)
改善
会社のレポジトリにコミットしたら、先輩から速攻のツッコミ!ありがたく受け取り修正しました!
つっこまれた部分
- xpath からオブジェクトを得る部分
- for 文の部分
- Firefox のjs では、Array.forEach(rows, function(row) { ... }) と書けるよ
- rows はArray ではないのでrows.each とは書けないよ
- Firefox のjs では、Array.forEach(rows, function(row) { ... }) と書けるよ
うぉ〜!勉強になります。ツッコミ頂いたら修正するのが礼儀だわ!ってことで修正。
修正版
// ==UserScript== // @name Slimtimer Report to Ticket // @namespace http://d.hatena.ne.jp/LukeSilvia/ // @description Create a link to ticket on the report by slimtimer. // @include http://slimtimer.com/tasks* // @require http://gist.github.com/raw/3242/1f990d36ad5cb261a481826411c4d7d6497ad5f3 // ==/UserScript== const BaseURL = "Trac のプロジェクトへのURL"; function toTicketAnchor(task, number) { var task_link = document.createElement('a'); task_link.setAttribute("target", "_blank"); task_link.setAttribute("href", BaseURL + number); task_link.appendChild(document.createTextNode(task.firstChild.data)); task.replaceChild(task_link, task.firstChild); } (function() { var data = $X('id("task-tbody")'); if(!data) return; var tbody = data[0]; var rows = tbody.rows Array.forEach(rows, function(row) { var task = row.cells[0]; if(task.firstChild.data.match(/#([0-9]+)/)) { toTicketAnchor(task, RegExp.$1); } }); } )();
感想
Greasemonkey は超楽しい!要は、コンテンスが自分専用にカスタマイズできるってわけですよね!不便だと思ったら、コンテンツの管理者に言うのではなく、自分でカスタマイズすればいいっていう話だ。
いや〜、グリモンだけでもjs を学ぶ価値は十分にあるな〜。js かっこいい!
次に作りたいグリモン
今度は、「http://slimtimer.com/report」の画面で同じことをやりたい。問題は、レポート結果がAjax レスポンスを使って描画されること。
フォームのonsubmit イベントでAjax リクエストを送るようになっている。「addEventListener」を使ってonsubmit にさっきのグリモンの処理を追加しようと思ったけどだめだった。Ajax のレスポンスが返ってくる前に処理が作動してしまう。なかなか難しい。