前回からGreasemonkeyによるアプリケーション開発の題材としてカレンダアプリケーションを挙げ、
今回はさらに二つの機能を追加したいと思います。
(1) 異なる月も表示する(2) 予定情報を登録/表示できるようにする
異なる月の表示機能
異なる月に移動できるようにするため、
「現在の月」
カレンダに追加する機能の検討
早速その実装をはじめようと思うところですが、
まず、
- 予定の年月日
- 予定の内容
予定情報の表示については、
- 予定情報が登録されている日付は色を変えて表示する
- 日付をカーソルキーなどで選択できるようにして、
選択日に予定情報が登録されていればその内容を表示する
また、
ということで、
- 「選択日」
を動かすようにする - 「選択日」
の月に応じたカレンダを表示する
という処理を実装することとします。
選択日の移動機能の実装
図1、
// ==UserScript==
// @name mini_calendar5
// @namespace http://gomaxfire.dnsdojo.com/
// @description the 5th mini calendar
// @include *
// ==/UserScript==
/* ユーティリティ関数郡の定義 */
/* 中略 */
/** document.getElementById() のエイリアス */
function $(id){
return document.getElementById(id);
}
/**
* Element#removeChild()のエイリアス
* $rmに与えた要素を削除する
*/
function $rm(element){
if(element && element.parentNode)element.parentNode.removeChild(element);
}
//----------------------------------------------------
// calendar application
//----------------------------------------------------
var calendar = (function(){ // -(1)
var frame = $div(); // -(2)
var table = null;
// calendar用処理群の中で共通利用する変数群を定義
var curDate = new Date(); //選択中の日付 -(3)
var TODAY = new Date();
var TODAY_YEAR = TODAY.getFullYear();
var TODAY_MONTH = TODAY.getMonth();
var TODAY_DATE = TODAY.getDate();
var CSS_PREFIX = "_gcal_";
function css(name){
return CSS_PREFIX + name;
}
// 選択中の日付セルを処理しやすくするために
// 日付セルにIDを付加する。そのIDをつくる関数 -(4)
function makeDateId(d){
return css([d.getFullYear(),
f(d.getMonth() + 1),
f(d.getDate())].join("-"));
function f(n){
return n < 10 ? "0" + n : n;
}
}
// calendar処理群としてまとめたので
// makeCalendarTableからmakeTableに名称変更
function makeTable(){
var DAYS = "Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" ");
$rm($(css("")));// 別の月に変更する場合のために以前の内容を破棄 -(2)
table = $table({id:css("")}); //-(2)
$add(frame, table); //-(2)
setMonthHeader();
setDayHeader();
setDates();
setStyle();
return frame;
function setMonthHeader(){
// curDateの年/月を使う
var year = curDate.getFullYear(); // -(3)
var month = curDate.getMonth() + 1; // -(3)
$add(table,
$add($tr({className:css("header")}),
$add($th({className:css("header"), colSpan:7}), year + "/" + month)));
}
// 中略
function setDates(){
// curDate自体を動かさないように、
// curDateを複製して表示用Dateオブジェクトとして利用する
var d = new Date(curDate); // -(3)
var nextMonth = d.getMonth() < 11 ? d.getMonth() + 1 : 0;
d.setDate(1);
d.setDate(-d.getDay() + 1);
// 中略
/**
* 年月日が一致したときのみ「今日」の見栄えにする
*/
function setClassName(td, d){
var buffer = [];
if(d.getMonth() == curDate.getMonth()){ // -(3)
buffer.push(css("onDate"));
buffer.push(css(DAYS[d.getDay()]));
} else {
buffer.push(css("outDate"));
}
if(d.getDate() == TODAY_DATE &&
d.getMonth() == TODAY_MONTH &&
d.getFullYear() == TODAY_YEAR){
buffer.push(css("today"));
}
td.className = buffer.join(" ");
}
}
function setStyle(){
var style =
<><![CDATA[
//中略
#_gcal_ td._gcal_select { //-(5)
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);
}
}
var gPanel = null;
function toggleCalendar(){
if(!gPanel) {
gPanel = $add($div({id:"_gpanel"}), makeTable());
$add(document.body, gPanel);
selectDate(curDate);
}
with(gPanel.style){
if(display != "block"){
display = "block";
} else {
display = "none";
}
}
}
/* 図2のA部分がここにくる */
// 外部インタフェースとなるオブジェクトを返す // -(1)
return {toggle:toggleCalendar,
nextMonth:makeGoMonthOffset(1),
previousMonth:makeGoMonthOffset(-1),
goToday:goToday,
nextDate:makeGoDateOffset(1),
previousDate:makeGoDateOffset(-1),
nextWeek:makeGoDateOffset(7),
previousWeek:makeGoDateOffset(-7)
};
})();
/* 図2のB部分がここにくる */
前回からの変更点はいくつかありますが、
(1) カレンダ処理をオブジェクトにまとめたこれまでのユーザスクリプトではカレンダ処理は
「実行時の月のカレンダを表示する」 という処理一つだけだったのですが、 今回からは選択日の移動処理や月の移動処理など複数の処理を定義する必要が出てきました。それら複数の処理を定義するために、 複数の関数を定義するわけですが、 それらの関数は次の二つの種類に分けられます。 (a) カレンダ処理を利用するために呼び出す関数。(b) 関数の定義のために定義した関数。カレンダ処理のために直接呼び出すことはない。
この二種類の関数定義がそのまま混在していると混乱の元ですし、
間違って (b) のタイプの関数を呼び出してしまうと意図せず (a) の処理に影響してしまう可能性もあります。そのため、 (a) のタイプの関数だけを外部から呼び出せるようにし、 (b) のタイプの関数を内部に閉じ込めて外部から呼び出すことができないようにするため、 オブジェクトの形にまとめるようにしました。 (2) 月の変更に応じたカレンダテーブルの削除処理のため、それを囲むdiv要素を加えた 月を変更したときはカレンダを表すテーブルを一旦削除し、
改めて変更した月に応じたカレンダテーブルを生成するように処理を変更しました。そのため、 再生成したカレンダテーブルを配置する 「場所」 が必要になり、 その役目をするdiv要素を置くことにしました。 (3) 選択日に応じてカレンダを生成するようにしたこれまではnew Date()によって得られる実行時の日付
(の月) に応じたカレンダを生成していましたが、 「選択日」 を変数curDateで表すこととして、 その日付に応じてカレンダを生成するようにしました。 (4) 日付セルを処理しやすくするため、IDを付加するようにした 「選択日」 がどれなのか、 表示上分かりやすくするために選択日を示す日付セルのclass属性の値を変えて見栄えを変えるようにしました。この日付セルの見栄えを変更する処理のため、 見栄えを変更すべき日付セルを特定する必要が必要になるわけですが、 この日付セルの特定を簡単に行えるようにするため各日付セルにID属性を付加することにしました。今回追加したユーティリティ関数 「$」 を使うことで、 IDを指定すれば簡単に所望の日付セル要素を取得できるからです。 (5) 選択日のスタイル定義を追加したカレンダテーブル中の日付セルのうちどれが
「選択日」 なのか一目で分かるよう、 スタイル定義を追加しました。
以上、
次に、
// ↓↓↓↓ここからA部分 ↓↓↓↓
/**
* dateで選択した日付に移動する
*/
function selectDate(date){ // -(1)
changeMonthIfNeed(date); // -(1-a)
unselect(curDate); // -(1-b)
select(date); // -(1-c)
function changeMonthIfNeed(date){
if(curDate.getMonth() != date.getMonth()){
curDate.setDate(1); // 意図せず二月分移動してしまうのを防ぐため
curDate.setMonth(date.getMonth());
curDate.setFullYear(date.getFullYear());
makeTable();
}
}
function unselect(date){
var preSelect = $(makeDateId(date));
if(preSelect){
preSelect.className =
preSelect.className.replace(/ _gcal_select/, "");
}
}
function select(date){
curDate = new Date(date);
var select = $(makeDateId(date));
select.className += " _gcal_select";
}
}
/**
* offset分だけ月を変更してカレンダテーブルの再生成をする
* 選択日は1日にする
* <外部インタフェース生成用>
*/
function makeGoMonthOffset(offset){ // -(2)
return function(){
if(!isShown())return;
var newDate = new Date(curDate);
newDate.setDate(1);
newDate.setMonth(curDate.getMonth() + offset);
selectDate(newDate);
};
}
/**
* offset分だけ日付を変更してカレンダテーブルの再生成をする
* <外部インタフェース生成用>
*/
function makeGoDateOffset(offset){ //-(3)
return function (){
if(!isShown())return;
var newDate = new Date(curDate);
newDate.setDate(newDate.getDate() + offset);
selectDate(newDate);
};
}
/**
* 実行時の日付に移動する
* <外部インタフェース用>
*/
function goToday(){ //-(4)
if(isShown())selectDate(TODAY);
}
/**
* 選択日の移動処理はカレンダを表示しているときのみ
* 実行したい。そのために表示中か否かを示す関数を用意。
*/
function isShown(){
return gPanel && gPanel.style.display != "none";
}
// ↑↑↑↑ここまでA部分 ↑↑↑↑
// 外部インタフェースとなるオブジェクトを返す(再掲)
return {toggle:toggleCalendar,
nextMonth:makeGoMonthOffset(1), //(2-a)
previousMonth:makeGoMonthOffset(-1), //(2-b)
goToday:goToday,
nextDate:makeGoDateOffset(1), //(3-a)
previousDate:makeGoDateOffset(-1), //(3-b)
nextWeek:makeGoDateOffset(7), //(3-c)
previousWeek:makeGoDateOffset(-7) //(3-d)
};
})();
// ↓↓↓↓ここからB部分 ↓↓↓↓
keybind("S-c", calendar.toggle);
keybind("S-n S-right S-down".split(" "), calendar.nextMonth);
keybind("S-p S-left S-up".split(" "), calendar.previousMonth);
keybind("f right".split(" "), calendar.nextDate);
keybind("b left".split(" "), calendar.previousDate);
keybind("n down".split(" "), calendar.nextWeek);
keybind("p up".split(" "), calendar.previousWeek);
keybind("S-t", calendar.goToday);
// ↑↑↑↑ここまでB部分 ↑↑↑↑
図2のA部分は選択日を移動する処理用の関数の定義で、
(1) selectDate:引数で指定した日付を「選択日」 とする この関数では(a)必要に応じて月を変更してカレンダテーブルを再生成し、
(b)現在選択中の日付を非選択にし、 (c)指定された日付を選択する、 という処理をしています。 (a)の処理は選択中の月と指定された月が異なる場合に実行するようにしています。
選択日curDateの日にちを1日に設定してから指定されたdateの月、
年に設定し、 カレンダテーブルを再生成しています。最初に日にちを1日に設定しているのは、 日にちによっては意図した月と異なる月になってしまう可能性があるためです。たとえば1月31日を表すDateオブジェクトに対しsetMonth(1)を実行する (Dateオブジェクトは1月を0として扱うため、 1は2月を表す) と、 2月31日ではなく、 3月3日に変更されます。このようにDateオブジェクトは存在しない日時を示さないように適切に処理する機能があるのですが、 カレンダの生成処理では指定した月に応じたカレンダを表示したいので、 このような月のジャンプが起こらないようにしたいわけです。そのため日にちを先に1日するようにしています。 (b)の処理は選択日から日付セル要素を特定し、
その日付セル要素のクラス名から選択日の見栄え用のクラス名を除去しています。 (c)の処理は指定された日付を新たに選択日に設定し、
該当する日付セル要素のクラス名に選択日の見栄え用のクラス名を付加しています。 なお、
前回も解説しましたが、 ここでも関数定義を分割することで、 コメントなしでもコードが読みやすくなるように工夫しています。 私は一つの関数の定義が長くなり、
数十行を越えてくると全体として何をやろうとしているのか自分でも分からなくなってしまうので、 このようにできるだけ関数定義は処理の単位で分割するようにしています。 ありがたいことにJavaScriptでは関数の内部で関数を定義できるので、
その関数の外部で使う必要のない (内部の) 関数は内部で定義しておくことができ、 長い定義になるような関数の分割がやりやすくなっています。 ただし、
デメリットもあって、 関数呼び出しが増えるとその分実行速度も低下します。実行速度を重視したい場合は分割の度合いを下げる必要があるかもしれません。 (2) makeGoMonthOffset:引数で指定された分だけ月を移動する関数を生成する本来必要なのは先月、
来月に移動するための関数なのですが、 その関数を生成するための関数を用意することでそれぞれの関数を定義する手間を省いています。引数に与えた数の分だけ現在の日付の月から移動する関数を生成するようにしているので、 先月への移動は-1、 来月への移動は1を引数として与えてそれぞれの関数を生成しています (2-a、 2-b)。 また今回の実装では先月、
来月に移動する処理だけを使っていますが、 前年、 来年に移動する処理の場合でも、 ±12を引数に与えれば同様の関数を生成することができます。 (3) makeGoDateOffset:引数で指定された分だけ選択日を移動する関数を生成するmakeGoMonthOffsetの日にち移動用版です。前の日、
次の日、 一週間前、 一週間後に移動するための関数を生成するために定義しました (3-a、 3-b、 3-c、 3-d)。 (4) goToday : 実行時の日付に移動する月を何度も移動しているとふと今日に戻りたくなるものです。そのための関数を用意しておきました。
以上が今回のユーザスクリプトの主要な部分でした。
最後のB部分は前回導入したkeybind関数を使って選択日の移動のためのキー操作を定義しています。
今回の実装により、
カレンダアプリケーションとしての便利度をちょっとあげることができたと思います。
予定情報の登録/表示機能
次は予定情報を登録/
(1) 登録済み予定情報の表示- 予定情報が登録されている日付のセルは色を変えて表示する
- 選択日に予定情報が登録されていればその情報を表示する
(2) 予定情報の登録- カレンダ画面下に追加用ボタンを配置する
- 追加用ボタンを押すと予定情報登録フォームが開く
- 予定情報登録フォームに情報を入力することで予定追加できる
(3) 登録済み予定情報の編集- 予定情報の表示欄に編集用ボタンをつける
- 編集用ボタンを押すと編集用フォームが開く
- 編集用フォームは登録済みの予定情報が入力済みになる
- 編集用フォームに更新情報を入力しすると更新できるようにする。
(4) 登録済み予定情報の削除- 予定情報の表示欄に削除用ボタンをつける
- 削除用ボタンを押すと予定情報が削除できる
- 削除の前に確認ダイアログを表示する
(キャンセルなら削除しない)
仕様として書き下すとたった四つの機能なのですが、
そのため、
予定情報管理機能の実装におけるポイント
今回のポイントは以下の四つです。
(1) 予定情報管理機能はカレンダオブジェクトとは別にスケジューラオブジェクトにまとめた一ページ目の
「異なる月の表示機能」 のユーザスクリプトでカレンダの表示に関わる処理をカレンダオブジェクトにまとめることにしましたので、 予定情報管理に関わる処理はそれとは別のスケジューラオブジェクトにまとめるようにしました。 (2) 予定情報の永続化にGM_setValue、 GM_ getValueを利用した Greasemonkeyにはいくつか組み込みの関数が定義されていますが、
そのうちのGM_ setValue、 GM_ getValueは文字列データの永続化に利用できる関数で、 予定情報の管理にもこの二つの関数を使いました。 (3) 画像データはdataスキームを使ってスクリプト内に埋め込んだ予定情報の追加ボタン、
編集ボタン、 削除ボタン用の画像データはdataスキームを使ってスクリプト内に埋め込むようにしました。こうすることでオフラインでも画像データを表示できるようになります。 (4) オブジェクト間の連携のためにイベント通知処理用オブジェクトを作成したポイント
(1) に付随することなのですが、 カレンダ表示と予定情報管理の機能をそれぞれ別のオブジェクトとして定義したので、 そのオブジェクト間で連携する処理が必要となります。これのオブジェクト間の連携処理に、 ユーザ操作に対するイベント処理と同様のオブザーバパターンを利用することとし、 そのためのイベント通知処理用オブジェクトを作成し、 これを利用することにしました。
これ以降、
カレンダオブジェクトの変更部分
図3はカレンダオブジェクトの変更部分を示しています。
// ==UserScript==
// @name mini_calendar6
// @namespace http://gomaxfire.dnsdojo.com/
// @description the 6th mini calendar
// @include *
// ==/UserScript==
/* ユーティリティ関数をここで定義するが省略 */
/**
* イベント通知処理用オブジェクト -(1)
*/
function Observer(){
this.init.apply(this, arguments);
}
Observer.prototype = {
init:function(){
this.listeners = [];
},
addListener:function(func){
this.listeners.push(func);
},
notify:function(){
var args = arguments;
this.listeners.forEach(function(func){
var result = func.apply(null, args);
});
}
};
/**
* cssのIDやclass名のprefix付加処理
* calendarとschedulerで利用するためそれらの外側に移動 -(2)
*/
var PREFIX = "_gcal_";
function css(name){
return PREFIX + name;
}
/**
* カレンダオブジェクトの定義
*/
var calendar = (function(){
// 中略
/**
* カレンダの日付セルにクラス名を追加するObserver -(3-1)
* <外部インタフェース用>
*/
var classNameObserver = new Observer();
function addSetClassNameListener(func){
classNameObserver.addListener(makeFunc(func));
function makeFunc(func){ // -(3-2)
return function(date, td){
var className = func(date);
if(className && td.className.indexOf(className) < 0){
td.className += " " + className;
}
};
}
}
/**
* 日付選択時の追加処理用Observer // -(3-4)
* <外部インタフェース用>
*/
var selectDateObserver = new Observer();
function addSelectDateListener(func){
selectDateObserver.addListener(func);
}
/**
* dateによって指定された日付セルにクラス名を追加する関数を生成する
* -(4-1) <外部インタフェース用>
*/
function makeAddClassNameToDateCell(className){
return function(date){
var cell = $(makeDateId(date));
if(cell && cell.className.indexOf(className)<0){
cell.className += " " + className;
}
selectDate(date);
};
}
/**
* dateによって指定された日付セルからクラス名を削除する関数を生成する
* -(4-2) <外部インタフェース用>
*/
function makeDeleteClassNameFromDateCell(className){
var regexp = new RegExp(" " + className);
return function(date){
var cell = $(makeDateId(date));
if(cell) cell.className = cell.className.replace(regexp, "");
};
}
/**
* 日付セル用のIDを生成する
*/
function makeDateId(d){
return css([d.getFullYear(),
f(d.getMonth() + 1),
f(d.getDate())].join("-"));
function f(n){
return n < 10 ? "0" + n : n;
}
}
// 中略
function makeTable(){
// 中略
// 日付セルの見栄えを設定するために
// クラス名を付加するリスナ関数の呼び出しも最後に行う
function setClassName(td, d){
// 中略
classNameObserver.notify(d, td); // -(3-3)
}
}
}
/**
* dateで選択した日付に移動する
*/
function selectDate(date){
changeMonthIfNeed(date);
unselect(curDate);
select(date);
// 中略
function select(date){
curDate = new Date(date);
var select = $(makeDateId(date));
select.className += " _gcal_select";
selectDateObserver.notify(date); // -(3-5)
}
}
// 中略
// 外部インタフェース用関数をオブジェクトにまとめて返す
return {nextMonth:makeGoMonthOffset(1),
// 中略
addSetClassNameListener:addSetClassNameListener,
makeAddClassNameToDateCell:makeAddClassNameToDateCell,
makeDeleteClassNameFromDateCell:makeDeleteClassNameFromDateCell,
addSelectDateListener:addSelectDateListener
};
})();
変更点のポイントは以下の四点です。
(1) イベント処理用にObserverオブジェクトを定義したObserverはオブジェクト間の連係動作用にObserverパターンを使うために定義したオブジェクトです。特定のイベントが発生したときに、
そのイベントを処理する関数 (リスナ関数) を呼び出す役目を持ちます。リスナ関数は前もってObserverオブジェクトに登録しておきます。 Observerオブジェクトは以下の関数を持ちます。
- addListener:リスナ関数を登録する
- notify:登録済みのリスナ関数を呼び出す
notify関数はイベントが発生した時点で呼び出すようにします。notify関数の引数をリスナ関数の呼び出し時にも与えるようにしています。リスナ関数の形式
(引数の渡し方、 返す値) は、 Observerごとに固定の形式に決める必要がありますが、 その形式はObserverを使う場所に応じて自由に決めればよいわけです。 このObserverオブジェクトを導入することで、
オブジェクト内に連携対象のオブジェクトを呼び出すコードを直接書く必要がなくなり、 メンテナンスしやすくなります。また、 さらに別のオブジェクトを連携対象にする場合もaddListener関数を使って追加登録するだけですむ、 というメリットもあります。 (2) 他のオブジェクトでも利用する関数をオブジェクトの外部で定義した関数cssは両者で共有して利用できるようにcalendarオブジェクトの外側で定義するようにしました。
(3) 見栄えの変更処理用に、リスナ関数の登録用関数を外部インタフェース用関数として定義した (1) のObserverオブジェクトを使って、 以下の二つのリスナ関数登録用の外部インタフェース用関数を定義しました。 - addSetClassNameListener
カレンダテーブルの生成の際に、
各日付セルを表すTD要素にクラス名を生成する処理をsetClassName関数で定義しています。ここで、 外部オブジェクトからも日付によって見栄えを変えることができるように、 クラス名を追加する処理を実行できるようにするため、 Obserberを使いました (3-1)。今回は 「予定情報が登録されている日付のセルは色を変えて表示する」 という仕様を実現するために使っています。リスナ関数はdateを引数とし、 追加したいクラス名を返す形式のものを登録するようにしました。リスナ関数の呼び出し結果のクラス名を日付セルに設定できるように、 リスナ関数に処理を追加した上でObserverに追加するようにしています (3-2)。そのため、 notify関数を呼び出すときは日付セルも引数として渡すようにしています (3-3)。 - addSelectDateListener
日付を選択したとき
(選択日を移動したときに) に、 何らかの処理を外部オブジェクトに実行させる仕組みを追加するためObserverを使いました (3-4)。日付選択の処理を定義しているselectDate関数の処理の最後で通知処理 (notify関数の呼び出し) を実行しています (3-5)。今回は 「選択日に予定情報が登録されていればその情報を表示する」 という仕様を実現するために使っています。リスナ関数はdateを引数とするものものを登録するようにしました。
(4) 外部から日付セルの見栄え変更を可能にする関数を外部インタフェース用関数として定義した予定情報を追加、
削除したタイミングでも日付セルの見栄えを変更できるようにするため、 日付セルにクラス名を付加する関数の生成関数makeAddClassNameToDateCellと、 クラス名を除去する関数の生成関数makeDeleteClassNameFromDateCellを定義しました。 両関数ともにクラス名を引数にとります。返す関数は、
日付オブジェクトを引数にとる関数で、 makeAddClassnameToDateCell関数は生成時に渡したクラス名を該当する日付セルに追加し、 makeDeleteClassNameFromDateCell関数は生成時に渡したクラス名を該当する日付セルから除去します。
スケジューラオブジェクトの定義
図4はスケジューラオブジェクトの定義部分を示しています。
var scheduler = (function(){
var events = load();
// 予定情報全体 日付をkeyにしたハッシュ。 -(1-1)
// 値は予定情報のIDをkeyにしたハッシュ。
var addEventObserver = new Observer(); // 予定情報を追加したときの付加処理のため
var deleteEventObserver = new Observer(); // 予定情報を削除したときの付加処理のため
function addAddEventListener(func){ // -(2-1)
addEventObserver.addListener(func);
}
function addDeleteEventListener(func){ // -(2-2)
deleteEventObserver.addListener(func);
}
//アイコンの画像バイナリデータ(base64エンコーディングしたもの)-(4)
var ADD_ICON = "data:image/png;base64," +
// 中略 (バイナリデータのbase64文字列が並ぶ)
var DELETE_ICON = "data:image/png,base64," +
// 中略 (バイナリデータのbase64文字列が並ぶ)
var EDIT_ICON = "data:image/png,base64," +
// 中略 (バイナリデータのbase64文字列が並ぶ)
function makeController(){
var cntr = $div({id:css("sche_")});
// 中略 (予定情報表示欄や予定情報登録用フォームのDOMツリーを生成)
}
function selectDate(date){
$(css("sche_year")).value = date.getFullYear();
$(css("sche_month")).value = date.getMonth() + 1;
$(css("sche_date")).value = date.getDate();
show(date);
}
function show(date){
var events = getEvents(date);
$rm($(css("sche_events")));
var eventsTable = $table({id:css("sche_events"),
cellSpacing:1,
cellPadding:0});
$add($(css("sche_events_frame")), eventsTable);
for(id in events){
event = events[id];
if(!eventsTable.firstChild){
$add(eventsTable,
$add($tr(),
$add($td(),"events:")));
}
var deleteButton = makeDeleteButton(event);
var editButton = makeEditButton(event);
var eventDate = [event.year, event.month, event.date].join("/");
var eventDescription = event.description ? event.description :"";
$add(eventsTable,
$add($tr(),
$add($td(),
$add($p(),eventDate),
$add($p(),eventDescription),
deleteButton, editButton)
)
);
}
// 中略 (makeEditButton、makeDeleteButtonの定義)
}
function getEventDate(event){
return new Date(event.year, event.month -1 , event.date);
}
function addEvent(event){
var index = eventIndex(event);
if(!index)return;
var list = events[index] || (events[index] = {});
if(!event.id)event.id = makeEventId();
list[event.id] = event;
save();
addEventObserver.notify(getEventDate(event));
// 中略(makeEventIdの定義)
}
function deleteEvent(event){
var index = eventIndex(event);
if(!index)return;
var list = events[index];
if(!list)return;
delete list[event.id];
if(!existsEvent(list)){
delete events[index];
deleteEventObserver.notify(getEventDate(event));
}
save();
show(getEventDate(event));
// 中略(existsEventの定義)
}
function getEvents(date){
return events[eventIndexByDate(date)] || {};
}
function hasEvents(date){ // -(3)
return (eventIndexByDate(date) in events);
}
// 中略(共通利用関数の定義)
function save(){ // -(1-2)
GM_setValue("events", events.toSource());
}
function load(){ // -(1-3)
return eval(GM_getValue("events", "({})")) || {};
}
// scheduler object
return {selectDate:selectDate,
makeController:makeController,
addAddEventListener:addAddEventListener, // -(2-1)
addDeleteEventListener:addDeleteEventListener, // -(2-2)
hasEvents:hasEvents // -(3)
};
})();
スケジューラオブジェクトの定義におけるポイントは以下の4点です。
(1) 予定情報はGM_setValue/ GM_ getValue関数を使って登録/ 参照するようにした Greasemonkeyではデータの永続化のための関数GM_
setValue、 GM_ getValueを利用することができます。 GM_
setValueはデータの登録処理を行うためのもので、 引数を二つとり、 一つ目の引数はデータの名前、 二つ目の引数はデータの値をとります。 GM_
getValueはデータの取得処理を行うためのもので、 引数を二つとり、 一つ目の引数はデータの名前、 二つ目の引数はデータが登録されていなかった場合に返すデフォルトの値 (文字列) をとります。一つ目の引数で指定した名前のデータが登録されていればその値を返します。 予定情報は(1-1)で示すように変数eventsで管理するようにしました。二重のハッシュ構造にし、
第一階層は日付を元にしたキーをとり、 第二階層は個々の予定情報を特定するIDをキーにとるようにし、 そこに予定情報を表すオブジェクトを登録するようにしました。今回のカレンダアプリケーションでは日付をひとつひとつ調べ、 その日付に対応する予定情報があれば表示処理を行う、 という処理を頻繁に実行するため日付の指定によって予定情報を簡単に取得できるようにしたかったため、 このような構造にしました。 この変数eventsの値をGM_
setValueで永続化すればGM_ getValueで再度予定情報を変数eventsに戻すことができるわけですが、 残念ながら永続化できるのは文字列データだけです。そのため、 オブジェクトを文字列データに変換する必要があるわけですが、 これにはJSON形式の文字列を使います。幸いなことにFirefoxではtoSource関数を呼び出すだけでオブジェクトのJSON文字列表現を生成できるので、 これをそのまま永続化用の文字列生成処理に使いました (1-2)。逆にJSON文字列をオブジェクトに戻すにはevalを使いました (1-3)。 (2) 予定情報の追加・削除後の処理のためのリスナ関数登録用関数を外部インタフェース用関数として定義した 外部オブジェクトに対し予定情報を追加、
削除したことを通知するための手段として外部インタフェース用関数addAddEventListener、 addDeleteEventListenerをObserverを使って定義しました (2-1、 2-2)。 四ページ目の
「カレンダオブジェクトの変更部分」 のポイント (4) の、 カレンダオブジェクトの日付セルの見栄え変更用関数 (を使って生成した関数) を、 予定情報の追加、 削除イベントのリスナ関数として登録することで、 予定情報を追加、 削除したタイミングで見栄えを変える機能を実現しています。 (3) 予定情報の有無を返す関数を外部インターフェース用関数として定義した四ページ目の
「カレンダオブジェクトの変更部分」 のポイント (3) にあげたとおり、 カレンダテーブル生成時に日付セルにクラス名を追加するための処理を行うためのインタフェースをカレンダオブジェクトに用意しました。そのインタフェースによって、 この予定情報の有無を返す関数を使ったリスナ関数を登録することで、 予定情報の有無により日付セルの見栄えを変える処理を実現しています (リスナ関数の生成および登録処理は図5に示しました。解説は次ページで行っています)。 (4) ボタン用画像はdataスキームを使って埋め込んだdataスキームはRFC2397で規定されているURLのスキームで、
バイナリデータを文字列として表現することでHTML (やJavaScript) 内に直接埋め込んだ形で記述することを可能にするものです。これを用いてボタン用画像を埋め込むようにしています。このようにdataスキームを使って画像データをユーザスクリプト内に埋め込むことでオフラインでもボタン用画像を表示することを可能にしています。 なお、
ボタン用画像は famfamfam. comのSilk Icons を、画像データのbase64化処理には[JavaScript]dataスキームURI生成 (画像データのBase64変換) を、それぞれ利用させていただきました。
カレンダオブジェクトとスケジューラオブジェクトの連結
図5はカレンダオブジェクトとスケジューラオブジェクトの連結処理部分を示しています。
/**
* カレンダの表示/非表示処理
* calendarとschedulerを使う処理なので
* その二つから切り離した
*/
var gPanel = null;
function toggleCalendar(){
setPanelIfNeed();
with(gPanel.style){
if(display != "block"){
display = "block";
} else {
display = "none";
}
}
function setPanelIfNeed(){
if(gPanel) return; // gPanelがあれば設定済み
connectCalendarAndScheduler();
gPanel =
$add($table({id:"_gpanel",
cellSpacing:0,
cellPadding:1}),
$add($tr(),
$add($td({id:css("frame")}), calendar.makeTable())),
$add($tr(),
$add($td({id:css("sche_frame")}), scheduler.makeController())));
$add(document.body, gPanel);
setStyle();
calendar.goToday();
function connectCalendarAndScheduler(){
var HAS_EVENTS_CLASS_NAME = css("has_events");
calendar.addSetClassNameListener(function(date){return scheduler.hasEvents(date) ? HAS_EVENTS_CLASS_NAME:"";});
// -(1)
calendar.addSelectDateListener(scheduler.selectDate);
// -(2)
scheduler.addAddEventListener(calendar.makeAddClassNameToDateCell(HAS_EVENTS_CLASS_NAME));
// -(3)
scheduler.addDeleteEventListener(calendar.makeDeleteClassNameFromDateCell(HAS_EVENTS_CLASS_NAME));
// -(4)
}
}
function setStyle(){
var style =
<><![CDATA[
//中略
#_gcal_sche_frame{
background-color:#C3D9FF;
padding:2px;
}
//中略
]]></>;
GM_addStyle(style);
}
}
// 最後にキーバインド定義をするが省略
カレンダテーブルの表示/
しかし、
表示/
この関数は四つの関数呼び出しを行っていますが、
(1) カレンダテーブル生成時の日付セルへのクラス名追加処理スケジューラオブジェクトの、
指定した日付に対する予定情報の有無を返す外部インタフェース用関数を用いて、 指定した日付に対し予定情報があれば 「予定情報あり」 を示すクラス名を返す関数を生成し、 それをカレンダオブジェクトの日付セルのクラス名生成イベント用リスナ関数として登録しています。この処理により、 カレンダテーブル生成時に予定情報がある日付セルの見栄えを変更することを実現しています。 (2) 日付セル選択時の予定情報表示処理スケジューラオブジェクトの指定した日付の予定情報を表示する関数を、
カレンダオブジェクトの日付選択イベント用リスナ関数として登録しています。この処理により、 選択日を移動したときに該当する日付の予定情報を表示する処理を実現しています。 (3) 予定情報追加時の日付セルへのクラス名追加処理カレンダオブジェクトの指定日の日付セルへのクラス名追加処理関数
(の生成関数によって生成される関数) をスケジューラオブジェクトの予定情報追加イベント用リスナ関数として登録しています。この処理により予定情報を追加したタイミングで対応する日付セルの見栄えを変更することを実現しています。 (4) 予定情報削除時の日付セルからのクラス名除去処理こちらは
(3) の 「追加」 が 「削除」 になっただけで、 ほとんど同じです。 カレンダオブジェクトの指定日の日付セルへのクラス名除去処理関数
(の生成関数によって生成される関数) をスケジューラオブジェクトの予定情報削除イベント用リスナ関数として登録しています。この処理により予定情報を削除したタイミングで対応する日付セルの見栄えを変更することを実現しています。
今回のまとめ
三ページ目
(1) 予定情報管理機能はcalendarオブジェクトとは別のオブジェクトにまとめた(2) 予定情報の永続化にGM_setValue、 GM_ getValueを利用した (3) 画像データはdataスキームを使ってスクリプト内に埋め込んだ(4) オブジェクト間の連携のためにイベント通知処理用オブジェクトを作成した
(1)
念のためですが、
(2)
次回の予告
次回はこのカレンダにGoogle Calendarに登録されている予定情報を表示させるようにしてみます。また、