ゼロと無限の間に

フリーでオープンソースなJavaScriptとかPHPとか。

ユーザ用ツール

サイト用ツール


php-tool-box:todo

文書の過去の版を表示しています。


PHP 1ファイルのみでDB不要のTODO管理ツール - Todo.php

コメントと更新履歴はゼロと無限の間のログ » Todo.phpへどうぞ。

MOONGIFT風に)みなさんはタスク管理にどんなものを使っているだろうか。Webのサービスでもインストール型のツールでも、TODO管理の方法は色々あるが、いざ探してみると意外に帯に短し襷に長しである。
Remember The Milkは高機能だが重いし、Tracは共有するには良いが個人で使うには冗長、iGoogleのTODOガジェットはシンプルでよいが痒いところに手が届かない。
それならいっそ、自分のサーバで手軽に管理できるTODOツールはどうだろうか。今日紹介するのはPHPが1ファイルのみ、しかもDBも不要なTODO管理ツール、「Todo.php」だ。

HTML5に対応

バージョン0.2.xまではXHTML 1.0 Strictでしたが、バージョン0.3からはHTML5にしました。(主に属性等をシンプルにしただけですけど ;-) )

使い方

下記のソースコードをコピーして適当な名前でPHPファイルとして保存し、Todoクラスの先頭で定義してある定数(const)をお好みで変更してください。また、定数に合わせてデータを保存するディレクトリを作ってください。

なお、このソースコードではJavaScirptとCSSとして、操作性を高めるためにPryn.js/cssを、カレンダーを使うためにYahho CalendarGCalendar Holidaysを、フッターをページ最下部に表示するためにYahho Sticky Footerを使っていますが、これら無しでもTodo.phpは動きます。使わない場合はこれらのJavaScriptとCSSファイルを削除してください。

サンプル

さらに進んだ使い方

メールサーバが使えるなら、mail2todoを組み合わせて使うと便利です。

ライセンス

ソースコード

<?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 ?>&amp;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>
php-tool-box/todo.1297173394.txt.gz · 最終更新: 2011/02/08 22:56 by dgbadmin

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki