あどけない話

インターネットに関する技術的な話など

LogUpdater

僕はよくネットワークや電話を使ってミーティングをします。その際、記録係は適当な web サーバに議事録を随時書き込み、参加者はそのページを見ることで、議事録を共有します。

僕が記録係の場合は、Emacs の tramp を使って、web サーバ上のファイルを書き換えます。Emacs 22 には tramp が標準で入っています。遠隔のファイル名は、こういう感じで指定します。

C-xC-f /サーバ名:パス

Emacs 使いでない人は、ローカルのファイルを scp で定期的にコピーするようです。sh だと、こんな感じでしょう。

% while true
do
scp ファイル名 サーバ名
sleep 10
done

リロードの問題

議事録のファイルは、どんどん更新されていきます。最新の議事録を見るために、参加者全員がブラウザのリロード・ボタンを定期的に押すのはバカげています。

そこで昔は、HTML のメタタグに refresh を仕込んでいました。

<meta http-equiv="refresh" content="10;">

Firefox だと快適です。しかし、IE だと、今見ている位置を覚えておいてくれないので、リロードの後はまた見たいところまでスクロールしないといけません。

JavaScript で解決

この問題を解決するために、JavaScript のプログラムを書いてみました。Firefox 2、IE 6/7、Safari 2/3 では動いているようです。

読む人

最初に start ボタンを押して下さい。これで、議事録が定期的にリロードされます。

読んでいる場所を覚えているので、リロード後もそれまでの場所まで自動的にスクロールします。スクロールの動作は速過ぎて、読んでいる人には単に留まっているように見えます。

一番最後を表示していると、リロード後に一番最後までスクロールします。今議論している部分を眺めておくのに便利です。

議事録を録る人

ファイルが3つ必要です。

  • HTML ファイル (名前はなんでもよい)
  • JavaScript ファイル (logupdater.js)
  • テキスト・ファイル (UTF-8; 議事録を書き込んでいく)

まず、以下の HTML をどこかの web サーバにコピーします。ファイル名は何でも構いません。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html 
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta http-equiv="content-script-type" content="text/javascript" /> 
<script type="text/javascript" src="logupdater.js"></script>
<title>ログファイル</title></head>
<body onload="LogUpdater.initialize('log');">
<div id="log"></div>
<input type="button" value="start" id="start" onclick="LogUpdater.toggle(); $('stop').removeAttribute('disabled'); $('start').setAttribute('disabled', 'disabled');" />
<input type="button" value="stop" id="stop" onclick="LogUpdater.toggle(); $('start').removeAttribute('disabled'); $('stop').setAttribute('disabled', 'disabled');" disabled="disabled" />
<input type="button" value="clear" onclick="LogUpdater.clear();" />
</body>
</html>

次に、このページの最後にある JavaScript のコードを "logupdater.js" というファイル名で同じディレクトリにコピーします。

これで準備 OK です。

後は、同じディレクトリに "log.txt" をいうファイルで議事録を書き込んで下さい。文字コードUTF-8 にして下さいね。

var LogUpdater = {
    _options: {
	interval: 10,
	file: 'log.txt'
    },
    initialize: function(id) {
	this._id = id;
    },
    _timerId: null, 
    toggle: function() {
	if (this._timerId === null) {
	    this._get();
	    var interval = this._options.interval * 1000;
	    this._timerId = setInterval(this._get.bind(this), interval);
	    window.status = 'LogUpdater started';
	} else {
	    clearInterval(this._timerId);
	    this._timerId = null;
	    window.status = 'LogUpdater stopped';
	}
    },
    _get: function() {
	var ajax = $Ajax();
	if (ajax !== null) {
	    ajax.onreadystatechange = function() {
		if (ajax.readyState == $Ajax.completeState) {
		    this._update(ajax);
		}
	    }.bind(this);
	    ajax.open('GET', this._options.file, $Ajax.async);
	    ajax.setRequestHeader('If-Modified-Since', 'Thu, 01 Jun 1970 00:00:00 GMT');
	    ajax.send(null);
	    window.status = 'Reloading...';
	}
    }, 
    _update: function(ajax) {
	var ctx = this._preScroll();
	var pre = document.createElement('pre');
	var text = $translate(ajax.responseText);
        pre.appendChild(document.createTextNode(text));
	this._clear();
	$(this._id).appendChild(pre);
	window.scrollTo(0, this._postScroll(ctx));
	window.status = 'Reloading...done';
	setTimeout(function() { window.status = ''; }, 1000);
	ajax.onreadystatechange = null; // IE's memory leak
    },
    _preScroll: function() {
	var ctx = {
	    top: 0,
	    isBottom: false,
	    doc: null
	};
	if (document.body.scrollTop !== 0) {
	    ctx.doc = document.body;
	    ctx.top = ctx.doc.scrollTop;
	}
	if (document.documentElement.scrollTop !== 0) {
	    ctx.doc = document.documentElement;
	    ctx.top = ctx.doc.scrollTop;
	}
	if ((ctx.doc !== null) &&
	    (ctx.top + ctx.doc.clientHeight == ctx.doc.scrollHeight)) {
	    ctx.isBottom = true;
	}
	return ctx;
    },
    _postScroll: function(ctx) {
	if (ctx.isBottom === true) {
	    return ctx.doc.scrollHeight;
	} else {
	    return ctx.top;
	}
    },
    _clear: function() {
	var disp = $(this._id);
	while (disp.firstChild !== null) {
	    disp.removeChild(disp.firstChild);
	}
    },
    clear: function() {
	this._clear();
	window.status = '';
    }
};

Function.prototype.bind = function(self) {
    var func = this;
    return function() {
	return func.call(self);
    };
};

function $(id) {
    return document.getElementById(id);
}

var $Ajax = (function() {
    if (window.XMLHttpRequest) {
        return function() { return new XMLHttpRequest(); };
    } else if (window.ActiveXObject) {
	return function() {
            try {
		return new ActiveXObject('Msxml2.XMLHTTP');
            } catch(e) {
		return new ActiveXObject('Microsoft.XMLHTTP');
            }
	};
    } else {
	return function() { return null; };
    }
})();

$Ajax.completeState = 4;
$Ajax.async = true;

var $translate = (function() {
    var ua = navigator.userAgent;
    if (ua.indexOf('MSIE') != -1) {
	return function(text) {
	    return text.replace(/\r?\n|\n/g, '\r\n');
	};
    } else if ((ua.indexOf('Safari') != -1) && (ua.indexOf('Version/2') != -1)) {
	return function(text) {
	    var esc = escape(text);
	    if (esc.indexOf('%u') < 0 && esc.indexOf('%') > -1) {
		return decodeURIComponent(esc);
	    } else {
		return text;
	    }
	};
    } else {
	return function(text) {
	    return text;
	};
    }
})();