僕はよくネットワークや電話を使ってミーティングをします。その際、記録係は適当な 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; }; } })();