GWTを使用したPukiwikiプラグインを作る

Pukiwikiにタスクの進捗を管理するページがある。最初の頃は表を手動で書き換えるようになっていて、めちゃめちゃ面倒だった。最近、listboxプラグインを導入して簡単に書き換えられるようにしたのだが、今度は見栄えが悪くなってしまった。そこで、見栄えと操作性を両立することができないものかと思い、GWTを使用したAjax的なlistboxプラグインを作ることにした。

調査

listboxプラグイン及びcheckbox2プラグインがどのような動きをしているのか調べてみた。

plugin_*_inline()

plugin_*_inline()では引数に対応したHTMLを生成している。このときページ内で何番目のプラグインかを覚えておくようにして、そのインデックスをJavaScriptに渡している。また、0番目のプラグインを表示させる前にJavaScriptを生成している。JavaScriptでは与えられたパラメータに応じてpluginを呼び出している。呼び出すときにはformを動的に生成してsubmitを呼び出すなどしている。受け渡されるパラメータはプラグインの種類、何番目のプラグインか、値、ページ名等。

plugin_*_action()

plugin_*_action()では渡されたパラメータを元にページの書き換えを行っている。ページの書き換えは現在のページの先頭から順に調べていって、該当するプラグインの位置まで来たら文字列を書き換えるという物。

設計

インターフェースとしては、文字orマークをクリックするとポップアップウィンドウが表示され、ポップアップウィンドウ内の選択肢をクリックするとページが書き換わるような感じにしたい。これを内部の動作と対応させると、plugin_*_inline()では、クリックするとJavaScriptを呼び出すようなHTMLを生成し、JavaScriptでは渡されたパラメータに応じてプラグインを呼び出すような感じになる。

JavaScriptの直書きは慣れていないので、Google Web Toolkit(GWT)を使用して楽をしたい。その為にはnativeなJavaScriptからGWTの生成したJavaScriptを呼び出す必要がある。これにはJNSIを使う。JNSIはGWTの内部で直接JavaScriptを扱いたい場合に使用する機能だ。通常はJavaからJavaScriptを呼び出すときに使うのだが、ちょっとしたtrickを使うとnativeなJavaScriptからGWTが生成したJavaScriptを呼び出すことができるらしい。

以上より実装すべきものは以下の通りとなる。

  • plugin_listbox3_action()
  • plugin_listbox3_inline()
  • ポップアップウィンドウ

実装

plugin_listbox3_action()

listboxプラグインを元に引数の変更などを行った。ほとんど元のソースと同じ。

function plugin_listbox3_action() {
	global $vars;
	$listBoxIndex = 0;
	$pagedata = '';
	$pagedata_old  = get_source($vars['refer']);
	foreach($pagedata_old as $line) {
		if(!preg_match('/^(?:\/\/| )/', $line)) {
			if (preg_match_all('/(?:&listbox3\(([^\)]*)\));/', $line, $matches, PREG_SET_ORDER)) {
				$paddata = preg_split('/&listbox3\([^\)]*\);/', $line);
				$line = $paddata[0];
				foreach($matches as $i => $match) {
					$opt = $match[1];
					if($vars['listBoxIndex'] == $listBoxIndex++) {
						//ターゲットのプラグイン部分
						$opt = preg_replace('/[^,]*/', $vars['optionIndex'], $opt, 1);
					}
					$line .= "&listbox3($opt);" . $paddata[$i+1];
				}
			}
		}
		$pagedata .= $line;
	}
	page_write($vars['refer'], $pagedata);
	return array('msg' => '', 'body' => '');
}
plugin_listbox3_inline()
function plugin_listbox3_inline()
{
	$listBoxIndex = plugin_listbox3_getNumber();
	if(func_num_args() == 2)
	{
		$options = func_get_args();
		$optionIndex = $options[0];
		return plugin_listbox3_getBody($listBoxIndex, $optionIndex);
	}
	return FALSE;
}

function plugin_listbox3_getNumber() {
	global $vars;
	static $listBoxIndexs = array();
	if (!array_key_exists($vars['page'],$listBoxIndexs))
	{
		$listBoxIndexs[$vars['page']] = 0;
	}
	return $listBoxIndexs[$vars['page']]++;
}

function plugin_listbox3_getBody($listBoxIndex, $optionIndex) {
	$options_html = plugin_listbox3_getOptions($listBoxIndex, $optionIndex);
	$body = ($listBoxIndex == 0) ? plugin_listbox3_getScript() : '';
	$body .= $options_html;
	return $body;
}

function plugin_listbox3_getOptions($listBoxIndex, $optionIndex) {
	global $vars;
	$page_enc = htmlspecialchars($vars['page']);
	$options = array(
<<<EOD
<button style="background-color:#ccffcc;font-size:x-large;border:none;cursor:pointer;" onclick="showOptions(this,$listBoxIndex,'$page_enc')"></button>
EOD
	,
<<<EOD
<button style="background-color:#ffffcc;font-size:x-large;border:none;cursor:pointer;" onclick="showOptions(this,$listBoxIndex,'$page_enc')"></button>
EOD
	,
<<<EOD
<button style="background-color:#ffcccc;font-size:x-large;border:none;cursor:pointer;" onclick="showOptions(this,$listBoxIndex,'$page_enc')">×</button>
EOD
	,
<<<EOD
<button style="background-color:#cccccc;font-size:x-large;border:none;cursor:pointer;" onclick="showOptions(this,$listBoxIndex,'$page_enc')"></button>
EOD
	);
	return $options[$optionIndex];
}

function plugin_listbox3_getScript() {
	return <<<EOD
<script type="text/javascript" language="javascript" src="listbox3/listbox3.nocache.js"></script>
EOD;
}
ポップアップウィンドウ

初めにnativeなJavaScriptからGWTが生成したJavaScriptを呼ぶためのトリック部分。initialize()ではポップアップウィンドウを表示させるメソッドをshowOptionsという関数に代入している。こうすることでイベントハンドラからshowOptions(this,listIndex,pageName);といった感じで呼び出すことができるようになる。GWT Exporterというライブラリを使うともっと簡単に書けるようになるらしいが、今回はこれで十分。

package hoge.client;

import com.google.gwt.core.client.EntryPoint;

public class Listbox3 implements EntryPoint {
	@Override
	public void onModuleLoad() {
		initialize();
	}

	private native void initialize() /*-{
		$wnd.showOptions = @hoge.client.Selector::showOptions(Lcom/google/gwt/dom/client/Element;ILjava/lang/String;);
	}-*/;
}

ポップアップウィンドウを表示させる部分。第一引数でイベントハンドラを渡してもらって、表示位置を取得している。イベントハンドラはElementで渡せるらしい。

package hoge.client;

import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DecoratedPopupPanel;
import com.google.gwt.user.client.ui.HorizontalPanel;

public class Selector {
	private static void showOptions(Element element, int listBoxId, String refer) {
		final DecoratedPopupPanel decoratedPopupPanel = new DecoratedPopupPanel(true);

		final HorizontalPanel horizontalPanel = new HorizontalPanel();
		final String[] options = { "○", "△", "?", "‐" };
		for (int optionIndex = 0; optionIndex < options.length; ++optionIndex) {
			final String option = options[optionIndex];
			final Button button = new Button(option);
			button.addClickHandler(new Poster(listBoxId, optionIndex, refer));
			horizontalPanel.add(button);
		}

		decoratedPopupPanel.setWidget(horizontalPanel);
		final int x = element.getAbsoluteLeft();
		final int y = element.getAbsoluteTop();
		decoratedPopupPanel.setPopupPosition(x, y);
		decoratedPopupPanel.show();
	}
}

プラグインを呼び出す部分。必要な引数をパラメーターに入れてGETをしている。

package hoge.client;

import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Window;

public class Poster implements ClickHandler {
	private final int listBoxIndex;
	private final int optionIndex;
	private final String refer;

	public Poster(int listBoxIndex, int optionIndex, String refer) {
		this.listBoxIndex = listBoxIndex;
		this.optionIndex = optionIndex;
		this.refer = refer;
	}

	@Override
	public void onClick(ClickEvent event) {
		Window.open("?listBoxIndex=" + listBoxIndex + "&optionIndex=" + optionIndex + "&plugin=listbox3&refer=" + refer, "_self", "");
	}
}

利用方法

GWTで作ったJavaScriptpukiwikiディレクトリ内に配置する。あとはWiki内で普通にプラグインを使うだけ。

問題点

  • 選択肢をハードコーディングしてしまっているが非常に汚い。しかし、これをWikiの書式側に持ってくるとパースが面倒になる。
  • 表の中で使用すると表がすかすかになってしまい見栄えが良くない。
  • スタイルシートが上書きされてしまった。

どうしよう・・・。