コメントと更新履歴はゼロと無限の間のログ » Todo.phpへどうぞ。
(MOONGIFT風に)みなさんはタスク管理にどんなものを使っているだろうか。Webのサービスでもインストール型のツールでも、TODO管理の方法は色々あるが、いざ探してみると意外に帯に短し襷に長しである。
Remember The Milkは高機能だが重いし、Tracは共有するには良いが個人で使うには冗長、iGoogleのTODOガジェットはシンプルでよいが痒いところに手が届かない。
それならいっそ、自分のサーバで手軽に管理できるTODOツールはどうだろうか。今日紹介するのはPHPが1ファイルのみ、しかもDBも不要なTODO管理ツール、「Todo.php」だ。
バージョン0.2.xまではXHTML 1.0 Strictでしたが、バージョン0.3からはHTML5にしました。(主に属性等をシンプルにしただけですけど
)
下記のソースコードをコピーして適当な名前でPHPファイルとして保存し、Todoクラスの先頭で定義してある定数(const)をお好みで変更してください。また、定数に合わせてデータを保存するディレクトリを作ってください。
なお、このソースコードではJavaScirptとCSSとして、操作性を高めるためにPryn.js/cssを、カレンダーを使うためにYahho CalendarとGCalendar Holidaysを、フッターをページ最下部に表示するためにYahho Sticky Footerを使っていますが、これら無しでもTodo.phpは動きます。使わない場合はこれらのJavaScriptとCSSファイルを削除してください。
<?php
/**
* Todo.php - A Simple Task Manager -
* @version 0.3.10
* @see http://0-oo.net/sbox/php-tool-box/todo
* @copyright 2008-2011 dgbadmin@gmail.com
* @license http://0-oo.net/pryn/MIT_license.txt (The MIT license)
*/
class Todo {
/** 文字コード */
const ENCODING = 'UTF-8';
/** サーバに保存するファイル名の文字コード */
//const FILE_NAME_ENCODING = 'UTF-8'; //Linuxその1
const FILE_NAME_ENCODING = 'EUC-JP'; //Linuxその2
//const FILE_NAME_ENCODING = 'Shift_JIS'; //Windows
/** TODOデータを保存するディレクトリ */
const DATA_DIR = 'data';
/** カテゴリ名として許可する正規表現 */
const CAT_REGEX = '^[^\\\\./:*?"<>|]{1,20}$';
/** バックアップの保存期限 */
const BACKUP_TIME = '-7 day';
/** 優先度の最大値 */
const PRI_MAX = 5;
public $cat;
public $cats;
public $list;
/**
* コンストラクタ
* @param string $cat カテゴリ
*/
public function __construct($cat) {
mb_internal_encoding(Todo::ENCODING);
mb_regex_encoding(Todo::ENCODING);
ini_set('default_charset', Todo::ENCODING); //HTTPヘッダーでの文字コード指定
ini_set('mbstring.strict_detection', true);
mb_substitute_character(0x005f); //変換できない文字は"_"にする
$this->cat = $this->_encode($cat);
}
/**
* 表示の準備
*/
public function setUp() {
if ($this->isValidCat()) {
if ($_REQUEST['delete'] && $this->_deleteCat()) {
} else {
if ($_POST['update']) {
$this->_updateList();
}
$this->list = $this->_getList();
}
}
$this->cats = $this->_getCategories();
}
/**
* カテゴリチェック
* @return boolean 許可されるカテゴリかどうか
*/
public function isValidCat() {
return mb_eregi(Todo::CAT_REGEX, $this->cat);
}
/**
* TODOリストのファイルパスを取得する
* @return string パス
*/
public function getPath() {
$cat = mb_convert_encoding($this->cat, Todo::FILE_NAME_ENCODING);
return Todo::DATA_DIR . '/' . $cat . '.txt';
}
/**
* 入力データの文字コードを正しく変換する
* @param $input string 入力データ
* @return string 文字コード変換後の入力データ
*/
private function _encode($input) {
return mb_convert_encoding($input, Todo::ENCODING);
}
/**
* カテゴリを全て取得する
* @return array 全てのカテゴリのカテゴリ名とファイルサイズ
*/
private function _getCategories() {
$h = openDir(Todo::DATA_DIR);
if (!$h) {
exit('data directory is not found.');
}
$cats = array();
$limit = date('YmdHis', strToTime(Todo::BACKUP_TIME));
while (false !== ($file = readDir($h))) {
if (is_dir($file)) {
continue;
}
$arr = explode('.', $file);
$path = Todo::DATA_DIR . '/' . $file;
if (count($arr) == 3) { //バックアップの場合
if ($arr[2] < $limit) { //期限切れは削除
unlink($path);
}
} else { //最新版の場合
$cat = mb_convert_encoding($arr[0], Todo::ENCODING, Todo::FILE_NAME_ENCODING);
$cats[$cat] = fileSize($path);
}
}
closeDir($h);
ksort($cats);
return $cats;
}
/**
* TODOリストを更新して保存する
*/
private function _updateList() {
$oldCat = new Todo($_POST['oldcat']); //変更前のカテゴリ
$newPath = $this->getPath();
if (is_file(strToUpper(__FILE__))) { //ファイルパスで大文字小文字を区別しない場合
$change = strCaseCmp($oldCat->cat, $this->cat);
} else {
$change = ($oldCat->cat != $this->cat);
}
if ($change && is_file($newPath)) {
return; //変更後のカテゴリが既に存在する場合は更新しない
}
$oldPath = $oldCat->getPath();
if ($oldCat->isValidCat() && is_file($oldPath)) {
rename($oldPath, $oldPath . '.' . date('YmdHis')); //バックアップ
}
foreach ($_POST['todo'] as $post) {
if ($post[1] != '') { //TODO未入力は削除
$data .= implode("\t", array_map(array($this, '_encode'), $post)) . "\n";
}
}
file_put_contents($newPath, $data);
}
/**
* TODOリストを取得する
* @return array TODOリスト
*/
private function _getList() {
$path = $this->getPath();
if (!is_file($path)) { //新規の場合
return array('');
}
$list = explode("\n", file_get_contents($path));
rsort($list); //優先度順でソート
return $list;
}
/**
* カテゴリを削除する
* @return boolean 削除できたかどうか
*/
private function _deleteCat() {
$path = $this->getPath();
if (!is_file($path) || fileSize($path)) { //todoが残っている場合は削除させない
return false;
}
$this->cat = null;
return unlink($path);
}
}
//----- HTMLレンダリング用のグローバル関数 -----
/**
* HTMLエスケープ
* @param string $val エスケープしたい文字列
* @return string エスケープした文字列
*/
function h($val) {
return htmlSpecialChars($val, ENT_QUOTES);
}
/**
* option要素を出力する
* @param string $val optionの値
* @param string $selected selectedにすべき値
*/
function echoOption($val, $selectedVal) {
if ($val == $selectedVal) {
$selected = 'selected="selected"';
}
echo "<option $selected>$val</option>\n";
}
//----------------------------------------------
$todo = new Todo($_REQUEST['cat']);
$todo->setUp();
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="<?php echo Todo::ENCODING ?>" />
<title>TODO - <?php echo h($todo->cat) ?></title>
<link rel="stylesheet" href="https://0-oo.googlecode.com/svn/pryn.css" />
<link rel="stylesheet" href="https://0-oo.googlecode.com/svn/yahho-sticky-footer.css" />
<style>
header, article, footer { display: block }
header { padding-top: 1em }
#bd, #add { font-size: 100% }
#cat { margin-bottom: 1em; font-size: 161.6% }
h1 { margin: 1em 0 0 }
a { text-decoration: none }
a:visited { color: #03c }
tr.row:hover { background-color: #79a }
th, td { border: solid #79a 1px }
td { padding: 1px 0 1px 1px }
select, input { margin: 0 }
select, input.date, footer { text-align: center }
select, #todo input { border-width: 0 }
select { width: 4.3em; height: 1.6em }
input.todo { padding-left: 0.5em; width: 20em }
input.date { width: 6em }
#update { text-align: right }
#update input { padding: 0.2em 2em; line-height: 1.6 }
li { padding-top: 0.5em; font-size: 131% }
li#add input { width: 5em }
</style>
<script>
//IEでHTML5の新要素を使えるようにする
document.createElement("header");
document.createElement("footer");
</script>
</head>
<body>
<div id="doc" class="yui-t2">
<header id="hd"><h1><a href="?">TODO</a></h1></header>
<div id="bd">
<div id="yui-main"><div class="yui-b">
<article>
<?php
if ($todo->isValidCat()) {
?>
<form method="POST">
<div id="cat">
カテゴリ : <input type="text" name="cat" value="<?php echo h($todo->cat) ?>" />
<input type="hidden" name="oldcat" value="<?php echo h($todo->cat) ?>" />
</div>
<!-- TODOリスト -->
<table id="todo">
<thead>
<tr><th>優先度</th><th>TODO</th><th>開始日</th><th>期限</th><th>状態</th></tr>
</thead>
<tbody>
<?php
$styleClasses = array('', 'todo', 'date han', 'date han');
$statuses = array('<!-- -->', '保留', '完了');
foreach ($todo->list as $i => $row) {
$task = explode("\t", $row);
if ($task[4] == $statuses[2]) { //完了は出力しない
continue;
}
?>
<tr class="row">
<!-- 優先度 -->
<td>
<select name="todo[<?php echo $i ?>][]">
<?php
for ($j = 1; $j < Todo::PRI_MAX + 1; $j++) {
echoOption($j, $task[0]);
}
?>
</select>
</td>
<!-- TODO、開始日、期限 -->
<?php
for ($j = 1; $j < 4; $j++) {
?>
<td>
<input type="text" name="todo[<?php echo $i ?>][]"
value="<?php echo h($task[$j]) ?>" class="<?php echo $styleClasses[$j] ?>" />
</td>
<?php
}
?>
<!-- 状態 -->
<td>
<select name="todo[<?php echo $i ?>][]" title="「完了」にするとリストからなくなります">
<?php
foreach ($statuses as $status) {
echoOption($status, $task[4]);
}
?>
</select>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
<div id="update"><input type="submit" name="update" value="更新" /></div>
</form>
<?php
} else if ($todo->cat) {
?>
<span class="error">
残念ですが、このカテゴリ名( <?php echo h($todo->cat) ?> )は使えません
</span>
<?php
}
?>
</article>
</div></div>
<!-- カテゴリリスト -->
<nav class="yui-b">
<ul>
<?php
foreach ($todo->cats as $cat => $fileSize) {
$href = '?cat=' . rawurlencode($cat);
?>
<li>
<a href="<?php echo $href ?>"><?php echo h($cat) ?></a>
<?php
if (!$fileSize) { //todoが無いカテゴリは削除できる
?>
<a href="<?php echo $href ?>&delete=do" title="カテゴリを削除する">[削除]</a>
<?php
}
?>
</li>
<?php
}
?>
<li id="add">
<form method="POST">
<div>
<input type="text" name="cat" />
<input type="submit" value="追加" title="カテゴリを追加する" />
</div>
</form>
</li>
</ul>
</nav>
</div>
<footer id="ft">
powered by <a href="http://0-oo.net/sbox/php-tool-box/todo">Todo.php</a>
</footer>
</div>
<script src="//0-oo.googlecode.com/svn/pryn.js"></script>
<script src="//0-oo.googlecode.com/svn/yahho-calendar.js"></script>
<script src="//0-oo.googlecode.com/svn/gcalendar-holidays.js" async="async" defer="defer"></script>
<script>
Pryn.addEvent(window, "load", function() {
YahhoCal.loadYUI();
YahhoCal.setMondayAs1st();
(function(input) {
var acs = new Pryn.ClassAccessor(input);
if (acs.hasClass("date")) {
Pryn.addEvent(input, "click", function() {
YahhoCal.render(YAHOO.util.Dom.generateId(input));
});
acs.addClass("clickable");
input.title = "クリックするとカレンダーを表示します";
}
}).foreach($T("input"));
});
</script>
</body>
</html>