前回はGreasemonkeyの基本的な使い方について解説しました。今回からは本題の通りGreasemonkeyによるアプリケーション開発の話を進めていきたいと思います。
どのページを開いていても、

カレンダを命令一発で表示できるとよいのですが、
まずはカレンダを表現するHTMLをそのままページに挿入してみる
まずは単純にその月のカレンダを表示することから取りかかります。HTMLのTABLE
<table>
<tr>
<th colspan="7">2007/8</td>
</tr>
<tr>
<th>S</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th>
</tr>
<tr>
<td>29</td><td>30</td><td>31</td><td>1</td><td>2</td><td>3</td><td>4</td>
</tr>
<tr>
<td>5</td><td>6</td><td>7</td><td>8</td><td>9</td><td>10</td><td>11</td>
</tr>
<tr>
<td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td><td>18</td>
</tr>
<tr>
<td>19</td><td>20</td><td>21</td><td>22</td><td>23</td><td>24</td><td>25</td>
</tr>
<tr>
<td>26</td><td>27</td><td>28</td><td>29</td><td>30</td><td>31</td><td>1</td>
</tr>
</table>
この形式のHTMLを実行時点の月に合わせて作成すればよいわけですが、
今回最初のユーザスクリプトは図3です。
// ==UserScript==
// @name mini_calendar1
// @namespace http://gomaxfire.dnsdojo.com/
// @description the 1st mini calendar
// @include *
// ==/UserScript==
var table = // (1)
<table border="1">
<tr>
<th colspan="7">2007/8</td>
</tr>
<tr>
<th>S</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th>
</tr>
<tr>
<td>29</td><td>30</td><td>31</td><td>1</td><td>2</td><td>3</td><td>4</td>
</tr>
<tr>
<td>5</td><td>6</td><td>7</td><td>8</td><td>9</td><td>10</td><td>11</td>
</tr>
<tr>
<td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td><td>18</td>
</tr>
<tr>
<td>19</td><td>20</td><td>21</td><td>22</td><td>23</td><td>24</td><td>25</td>
</tr>
<tr>
<td>26</td><td>27</td><td>28</td><td>29</td><td>30</td><td>31</td><td>1</td>
</tr>
</table>;
var div = document.createElement("div"); // (2)
div.innerHTML = table.toSource(); // (3)
document.body.insertBefore(div, document.body.firstChild); // (4)
(1)でカレンダを表すHTMLをE4Xを使って変数tableにセットしています。E4XはECMAScriptにXMLを表現する文法とセマンティクスを追加する拡張仕様です。仕様について詳しくはECMAのページに載っているドキュメントに示されていますが、
(2)でdiv要素を作成し、
このユーザスクリプトを実行すると、

最初の単純なユーザスクリプトとしてはできは上々だと思いますが、
(1) 常に2007年8月に固定のカレンダである。実行時の月のカレンダにしたい。(2) ページの一番上に表示されてしまい、スクロールすると見えなくなってしまう・ 見栄えがよくない。 (3) 常にカレンダが表示されているのは邪魔である。必要なときだけ表示するようにしたい。
上から順に不満を解決していきたいと思います。
実行時の月のカレンダにする
実行時の月のカレンダにするには、
前ページでで示したスクリプト
一般に、
/**
* document.createElement() +アルファな関数
* attrsに指定した属性を設定し、stylesに指定したCSSプロパティを設定する
*/
function $tag(tagName, attrs, styles){
var tag = document.createElement(tagName);
if(attrs){
for(a in attrs){
if(attrs.hasOwnProperty(a)){
tag[a] = attrs[a];
}
}
}
if(styles){
for(a in styles){
if(styles.hasOwnProperty(a)){
tag.style[a] = styles[a];
}
}
}
return tag;
}
/**
* $tagのショートカット定義
* $div()でdiv要素を生成できるようにする。
* 引数は$tagの第2引数、第3引数をそのまま第1引数、第2引数として利用できる。
*/
"div p span a img table tr th td form label input textarea".split(" ").forEach(function(tagName){
var func = function(attrs, styles){
return $tag(tagName, attrs, styles);
};
eval("$" + tagName + "= func;" );
});
/**
* document.createTextNode()のエイリアス
*/
function $text(text){
return document.createTextNode(text);
}
/**
* Element#appendChild() +アルファな関数
* 第1引数の要素の末尾要素として第2引数以降で指定する要素を追加する
* Element.prototypeの関数として定義するほうがスマートになりそうだが、
* GreasemonkeyではElement要素を直接扱えないのでこの定義方法をとった。
*/
function $add(parent, children){
if(arguments.length < 2) return "";
for(var i=1, child; child=arguments[i];i++){
if(typeof child != "object"){
child = $text(child+"");
}
parent.appendChild(child);
}
return parent;
}
これらのユーティリティ関数を使ったところで実行速度が上がる訳ではありません。むしろ多少下がるでしょう。しかし込み入ったツリーを作るプログラムでは見た目上分かりやすくなる方が多少の速度低下よりもスクリプトのメンテナンス性が高くなると考え、
使い方の具体例を図6に示します。
表現したいHTML:
<form action="http://hoge.foo.com">
<label>label<input type="checkbox" /></label>
<input type="submit" value="send" />
</form>
document.appencChild/document.craeteElementを使った場合:
var form = document.createElement("form");
form.action = "http://hoge.foo.com";
var label = document.createElement("label");
label.textContent = "label";
var checkbox = document.craeteElement("input");
checkbox.type = "checkbox";
var submit = document.createElement("input");
submit.type = "submit";
submit.value = "send";
form.appendChild(label);
label.appendChild(checkbox);
form.appendChild(submit);
document.body.appendChild(form);
$add, $tagの派生関数群を使った場合
$add(document.body,
$add($form({action:"http://hoge.foo.com"}),
$add($label(),
"label",
$input({type:"checkbox"})),
$input({type:"submit", value:"send"})));
いかがでしょう。階層構造も分かりやすいし、
さて前置きが長くなってしまいましたが、
// ==UserScript==
// @name mini_calendar2
// @namespace http://gomaxfire.dnsdojo.com/
// @description the 2nd mini calendar
// @include *
// ==/UserScript==
// 図5のユーティリティ関数群をここで定義するが省略
/** 実行時の月のカレンダを表すtable要素を生成する */
function makeCalendarTable(){
var table = $table({border:1});
setMonthHeader();
setDayHeader();
setDates();
return table;
function setMonthHeader(){
var d = new Date();
var year = d.getFullYear();
var month = d.getMonth() + 1;
$add(table,
$add($tr(),
$add($td({colSpan:7}), year + "/" + month)));
}
function setDayHeader(){
var days = "S M T W T F S".split(" ");
var tr = $tr();
days.forEach(function(day){
$add(tr,
$add($th(), day));
});
$add(table, tr);
}
function setDates(){
var d = new Date();
var nextMonth = d.getMonth() < 11 ? d.getMonth() + 1 : 0;
d.setDate(1); // (3)-1
d.setDate(-d.getDay() + 1); // (3)-2
var finish = false;
for(;;){
var tr = $tr();
for(var i=0;i<7;i++){ // (3)-3
$add(tr, $add($td(), d.getDate()));
d.setDate(d.getDate() + 1); // (3)-4
if(d.getMonth() == nextMonth) finish = true; // (3)-5
}
$add(table, tr);
if(finish)break; // (3)-6
}
}
}
document.body.insertBefore($add($div(), makeCalendarTable()),
document.body.firstChild);
図7のmakeCalendarTable関数で実行時の月のカレンダを表すtableタグをルートとしてDOMツリーを生成しいます。内部の処理は三つの関数内関数定義に分離して読みやすいようにしました。
- (1)
setMonthHeader - "年/月"の形式で月を表すヘッダを生成する関数
- (2)
setDayHeader - 曜日を表すヘッダを生成する関数
- (3)
setDates - カレンダの本体である日にちを表すセル要素を生成する関数
(1)
まず、
最後の行でdiv要素にmakeCalendarTable()で生成したカレンダのtableを追加し、
見栄えを調整する
第2の不満は
「ページの一番上に表示されてしまい、
でした。これはCSSを設定することで解決できます。GreasemonkeyではCSSを追加設定するためのGM_
図7のユーザスクリプトのmakeCalendarTable関数の処理に、
// ==UserScript==
// @name mini_calendar3
// @namespace http://gomaxfire.dnsdojo.com/
// @description the 3rd mini calendar
// @include *
// ==/UserScript==
// 図5のユーティリティ関数群をここで定義するが省略
function makeCalendarTable(){
// 関数内で共通に利用する変数を定義 -(1)
var CSS_PREFIX = "_gcal_";
var DAYS = "Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" ");
var TODAY = new Date();
var TODAY_DATE = TODAY.getDate();
var TODAY_MONTH = TODAY.getMonth();
var table = $table({id:css("")});
setMonthHeader();
setDayHeader();
setDates();
setStyle();
return table;
/**
* 表示中のページ内のCSSで使われているクラス名との衝突をさけるため、
* 適当にプレフィックスをつけることにする。そのための補助関数。 -(1)
*/
function css(name){
return CSS_PREFIX + name;
}
function setMonthHeader(){
var d = new Date();
var year = d.getFullYear();
var month = d.getMonth() + 1;
// -(1)
$add(table,
$add($tr({className:css("header")}),
$add($th({className:css("header"), colSpan:7}), year + "/" + month)));
}
function setDayHeader(){
var tr = $tr({className:css("dayNames")});
// 曜日名を示す配列は日にちを示すtd要素のclass名にも
// 流用するために外側で定義した -(1)
DAYS.forEach(function(day){
$add(tr,
$add($th({className:[css(day),css("dayName")].join(" ")}),
day.substring(0,1)));
});
$add(table, tr);
}
function setDates(){
var d = new Date();
var nextMonth = d.getMonth() < 11 ? d.getMonth() + 1 : 0;
d.setDate(1);
d.setDate(-d.getDay() + 1);
var finish = false;
for(;;){
var tr = $tr();
for(var i=0;i<7;i++){
var td = $add($td(), d.getDate());
setClassName(td, d); // 日にちによって見栄えを変える -(1)
$add(tr, td);
d.setDate(d.getDate() + 1);
if(d.getMonth() == nextMonth) finish = true;
}
$add(table, tr);
if(finish)break;
}
/**
* 日にち表現用td要素を、その日にちによって見栄えが変わるよう
* class属性を設定する -(1)
*/
function setClassName(td, d){
var buffer = [];
if(d.getMonth() == TODAY_MONTH){
buffer.push(css("onDate"));
buffer.push(css(DAYS[d.getDay()]));
if(d.getDate() == TODAY_DATE){
buffer.push(css("today"));
}
} else {
buffer.push(css("outDate"));
}
td.className = buffer.join(" ");
}
}
/**
* GM_addStyle関数を使ってCSSを設定する -(2)
*/
function setStyle(){
var style =
<><![CDATA[
#_gpanel{
margin:0px;
padding:0px;
position:fixed;
top:0;
left:0;
z-index:999999;
width:10px;
background:transparent;
border-collapse: separate;
}
// 長いので中略
#_gcal_ td._gcal_today {
color : gold;
font-weight : bold;
border-top : 1px solid white;
border-left : 1px solid white;
border-bottom : 1px solid #666666;
border-right : 1px solid #666666;
background-color:#FFFFEE;
}
]]></>;
GM_addStyle(style);
}
}
// CSSで左上隅に配置するようにしたので、bodyの末尾要素に追加してもよい。
// そのためコーディング量が少なくてすむinsertBeforeではなくappendChildにした。
document.body.appendChild($add($div({id:"_gpanel"}), makeCalendarTable()));
基本的な処理方法は図7と変わっていません。変更点は大きく見ると2点で、
図8内に
以上で第2の不満も解消できました。
キーの押し下げでカレンダの表示/非表示を切り替える
第3の不満は
「常にカレンダが表示されているのは邪魔である。必要なときだけ表示するようにしたい。」
でした。
表示/
図8のユーザスクリプトの最後の部分をこの処理方式をとるよう、
/**
* documentに対するkeydownイベントの処理設定をする。
* 第1引数でキーを指定し、
* 第2引数で指定されたキーが押し下げられたときに実行する関数を指定する。
* ただし、INPUT要素、TEXTAREA要素に対するキーイベントの場合は何もしない。
*/
var keybind = (function(){
function add (phrase, func){
if(phrase instanceof Array){
phrase.forEach(function(p){
add(p, func);
});
} else {
document.addEventListener("keydown",
function(event){
var tagName = event.target.tagName;
if(phrase == code(event) &&
!tagName.match(/(INPUT|TEXTAREA)/i)){
func();
event.preventDefault();
event.stopPropagation();
}
}, true);
}
}
function code(event){
var code = [];
if(event.shiftKey){
code.push("S");
} else if(event.ctrlKey){
code.push("C");
} else if(event.altKey || event.metaKey){
code.push("M");
}
code.push(kc2char(event.keyCode));
return code.join("-");
function kc2char(kc){
function between(a,b){
return a <= kc && kc <= b;
}
var _32_40 = "space pageup pagedown end home left up right down".split(" ");
var kt = {
8 : "backspace",
9 : "tab" ,
13 : "enter",
16 : "shift",
17 : "control",
27 : "escape",
46 : "delete",
};
return (between(65,90) ? String.fromCharCode(kc+32) : // a-z
between(48,57) ? String.fromCharCode(kc) : // 0-9
between(96,105) ? String.fromCharCode(kc-48) : // num 0-9
between(32,40) ? _32_40[kc-32] :
kt.hasOwnProperty(kc) ? kt[kc] :
kc);
}
}
return add;
})();
var gPanel = null;
/**
* カレンダの表示/非表示を切り替える。
* 最初に表示するときまでカレンダの生成処理はしないようにする。
* そのためカレンダのフレームを表す変数gPanelの初期値をnullで定義しておく。
*/
function toggleCalendar(){
if(!gPanel){
gPanel = $add($div({id:"_gpanel"}), makeCalendarTable());
document.body.appendChild(gPanel);
}
with(gPanel.style){
if(display != "block"){
display = "block";
} else {
display = "none";
}
}
}
keybind("S-c", toggleCalendar);
はじめに、
keybind関数の利用方法は簡単で、
カレンダの表示/
以上で第3の不満も解決できました。
今回のまとめ
最初は質素でしかも2007年8月に固定のカレンダを表示するだけのユーザスクリプトでしたが、
今回のポイントは以下の2点です。
(1) スクリプトが読みやすく/書きやすくなるよう、 ユーティリティ関数を定義するなどの工夫をする (2) CSSで見栄えを設定することはできるが、表示中に含まれるページ内のCSS定義とパッティングしないように工夫する
(1)
(2)
次回の予告
次回はこのカレンダを