ローカル環境でも動くタスク管理ツールを作ってみることにします。
以下の環境で動作確認をしています。
環境: Windows パソコン
背景 ~ かんたんに使えるタスク管理ツールが欲しい!
個人でのプログラミングや小規模なプロジェクトなどの作業をしていると、ローカル環境で軽快に動く、タスク管理ツールがないかと思うことがあります。
休日など、まとまった時間が確保できるとき、趣味や勉強などでやりたかったこと、できることをすべて書き出して、設定した期日までに進められるだけ進めておきたい。
期限を設定しないとだらけてしまうので、こなせそうな項目を書き出して作業バラシを行い、着手できるところだけでも進捗させてしまいたい、といった状況のときです。
簡単に使えるかんばん方式のツールがあるとよいのですが、ネット検索をすると、ユーザー登録やログイン、インストール、支払いなど、余計な作業が増えてしまうものばかりです。
ということで、「かんばん」(カード、付箋紙)を扱うことができるタスク管理ツールを作ってみることにします。
一番重要なポイントは、ユーザーインターフェースの部分程度です。すると、Python などを使うまでもなく、JavaScript で作るのが簡単で最適となりそうです。
また、インターネット環境やサーバーなど、環境の制約は最小限とし、ローカル Web アプリとしてシンプルに使えるようにしたいです。したがって、データの保存については多少、工夫が必要そうです。
設定方法
① パソコンにフォルダを作ってください。
例: c:\user\task_management1
② ①のフォルダ内に、”kanban_board1.html” 等の名前でテキストファイルを作成し、下記のサンプルスクリプトをコピー&ペーストして保存してください。
③ ②の kanban_board1.html をダブルクリックしてください。
→ タスク管理ツールの画面がブラウザに表示されたら、設定まで完了です!
使い方
④ JavaScript のサンプルを以下に貼っておきますので、まずはカードを作成してドラッグしてみてください。(以下は、データの読み出しなど一部の機能は動作しません。)
おもな使い方はつぎのとおりです。
・ 3つのリスト欄にある「+」ボタンをクリックすることで、カード(かんばん)を作成、追加できます。
・ カードには文字の入力が可能です。
・ ドラッグ&ドロップにより、リスト間でカードを移動させることができます。
・ ボードのタイトル文字、各リスト欄のタイトル文字についても、編集できます。
・ カードを「Trash Bin」(ゴミ箱)にドラッグすることで、削除できます。
・ 「clear」ボタンで、ボード(kanban board、画面全体)の内容を初期化できます。
・ 「save」ボタンで、書き込んだ内容をダウンロードできます。
・ ダウンロードを実行すると、Windows の「ダウンロード」フォルダにファイル(*.js)が保存されます(Windows の場合)。
・ ①のフォルダ内に、ダウンロードしたファイルを移動することで、ダウンロードしたデータを読み出すことが可能です。
※ ダウンロードしたファイルは、JavaScript のファイルとして再度読み込めるようにするために、ファイル名指定で *.js としています。js ファイルの内容はテキスト形式としていますので流用や編集が可能です。
・ ボード(画面全体)は、20種類まで、保存および読み出すことができます。
・ 左上のリストボックスの番号をクリックすることで、①のフォルダに保存されている、対応するボードのデータを読み出すことが可能です。
・ 複数のボードを使う場合は、まず、画面左上でボードの番号を選択します。この後、カード等を追加します。「save」ボタンをクリックすると、ボード番号に対応した js ファイルをダウンロードできます。このファイルを①に移行することで、複数のボードのデータを読み出して使うことが可能です。
読み出したデータについて、上記と同様、編集、保存、カードの移動等が可能です。
うまく動いたら
・ うまく動いたら、タスク管理を試しで運用してみてください。
・ たとえば、身の回りの課題、気にかかっている事柄を書き出してタスクバラシをしてみてください。また、タスク管理ツールをどのように使うとよさそうか、生産性を上げるにはどうなっていればよいか、など、アイディア出しをしてみてください。
今日中に終わらせたいことや思いついたアイディアを実行に移していくなど、面白い活用が考えられると思います。
・ カードをいくつか書き出してグループ分けを進めていくと、ボードやリスト欄のタイトルを変えたほうがよさそうな場合が出てきます。この場合は、タイトル等も変えて、ボードごとに、もっともしっくりくる形、より的確な形に再編集してみてください。何をやりたかったのか、どうすればよいかなど、狙いをより明確化していくことが可能になります。
・ 上記の HTML をブラウザのお気に入りに保存しておくことで、タスク管理がかんたんに可能になります。
・ JavaScript や HTML がわかる方、興味のある方は、スクリプトを編集して、もっと洗練したデザインに修正してみてください。今回のスクリプトは、下記の関連リンクの機能を組み合わせ、さらに発展させた内容になっています。
・ Raspberry Pi などでローカルネットワーク内での Linux 環境のある方、Python が使える方は、Web アプリ化したり、保存したデータを自動処理する等のアレンジも可能です。
ユーザーごとに ID を割り振って Web サーバー化すれば、複数のユーザー間でタスクのやり取りをしたり、カレンダーと連携させるなども可能になると思います。
まとめ
タスク管理ツールを作成してみました。
今回は、ローカル Web アプリとして作成したので、保存等の機能には制約があります。とはいえ、ユーザ登録やログインなど、余計な手間は一切不要で、タスク管理ツールがすぐに使えるようになります。
また、Free でオープンソースのツールですので、ユーザーインターフェースを Web サーバーと連携させたり、データをカレンダーと連携させたり、ドラッグ&ドロップやデータ保存の機能を他に活用したり、知恵次第でどのようにでも発展させていくことが可能です。
小規模ですが、無料で自由にカスタマイズできる、デジタルトランスフォーメーション、DX が実現します。プログラミングが得意な方であれば、もっと想像を超えたアイディアを実現できるのではないかと思います。
他にも、ローカルでも動く Web アプリ、無料ツールのスクリプトなどをまとめ、公開しています。興味のある方は以下の関連リンクなども参照してみてください。
関連リンク
・ ブラウザで動くお絵描きアプリ 【JavaScript】
・ ブラウザで動く音楽プレーヤー 【JavaScript & Python】
・ ブラウザ上で3次元モデルを作成してみる 【JavaScript & Three.js】
・ HTML のカレンダーを出力する 【Python】
・ Python で掲示板を作ってみる 【Windows 版】
サンプルスクリプト
タスク管理ツール kanban_board1.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>kanban_board v0.1</title>
<style>
html, body, textarea, input {
font-family: helvetica, arial, sans-serif, 'Meiryo UI', Meiryo;
}
body {
background-color: #f7faff;
}
.side-by-side1 {
display: flex;
justify-content: space-between;
width:100%;
}
.header1 {
display:flex;
width:100%;"
}
.body1 {
display:flex;
justify-content: space-between;
width:100%;
}
.list1 {
flex: 1;
margin: 6px;
padding: 8px;
background-color: #f4f4f4;
border: 1px solid #888888;
box-sizing: border-box;
border-radius: 6px 6px 6px 6px;
min-height: 400px;
}
.trash1 {
padding: 0px;
background-color: #f4f4f4;
border: 1px solid #888888;
box-sizing: border-box;
border-radius: 6px 6px 6px 6px;
height:30px;
width:80px;
font-size:13px;
margin-left:5px;
margin-right:5px;
display:flex;
justify-content:center;
align-items:center;
}
.card1 {
margin: 7px;
padding: 5px;
background-color: #ffffff;
border: 1px solid #888888;
position: relative;
border-radius: 5px 5px 5px 5px;
}
.card1 textarea {
width: 100%;
box-sizing: border-box;
resize: none;
border: none;
background: none;
outline: none;
}
.textbox0 {
background-color: transparent;
border: none;
color: #111111;
font-size:16px;
height:30px;
margin-left:5px;
margin-right:5px;
flex-grow:1;
}
.textbox1 {
background-color: transparent;
border: none;
font-size:16px;
width:100%;
}
.button0 {
width:60px;
height:30px;
border:1px solid #000000;
background: #e4e4e4;
margin-left:5px;
margin-right:5px;
}
.button1 {
width:25px;
height:25px;
border:1px solid #cccccc;
background: #f4f4f4;
}
.select1 {
width:40px;
height:30px;
text-align: center;
}
</style>
<script src="./kanban_board01.js" onerror="err1()"></script>
<script>
document.addEventListener('DOMContentLoaded', init1);
window.onload = load1;
let n1_card = 0; // number of cards
let busy1 = -1;
function err1() {
var str1_board = "";
}
function init1() {
let lists1 = document.querySelectorAll('.list1');
lists1.forEach( addListEventListeners1 );
lists1 = document.querySelectorAll('.trash1');
lists1.forEach( addListEventListeners1 );
document.getElementById('select1').addEventListener('keyup', load1 );
document.getElementById('select1').addEventListener('click', load1 );
}
function addListEventListeners1( list1 ) {
list1.addEventListener('dragover', dragover1);
list1.addEventListener('drop', drop1);
}
function dragover1( event1 ) {
event1.preventDefault();
}
function drop1( event1 ) {
event1.preventDefault();
let id1 = event1.dataTransfer.getData('text');
let obj1 = document.getElementById(id1);
if (event1.target.id.includes("list_")) {
event1.target.appendChild(obj1);
} else if (event1.target.id == "trash1" ) {
event1.target.appendChild(obj1);
event1.target.removeChild(obj1);
}
}
function dragstart1( event1 ) {
event1.dataTransfer.setData('text', event1.target.id);
}
function addCard1( list1, str1 ) {
let obj1 = document.getElementById( list1 );
let card1 = createCard1( str1 );
obj1.appendChild( card1 );
let textarea1 = card1.querySelector('textarea');
adjust_height1( textarea1 );
}
function createCard1( str1 ) {
n1_card = n1_card + 1;
let id1 = 'card_id_' + n1_card;
let card1 = document.createElement( 'div' );
card1.className = 'card1';
card1.id = id1;
card1.draggable = true;
let textarea1 = document.createElement( 'textarea' );
textarea1.value = str1;
textarea1.readOnly = true;
textarea1.addEventListener('click', function() {
textarea1.readOnly = !textarea1.readOnly;
if (!textarea1.readOnly) {
textarea1.focus();
}
adjust_height1( textarea1 );
});
textarea1.addEventListener('input', adjust_height1( textarea1 ));
textarea1.addEventListener('blur', adjust_height1( textarea1 ));
card1.appendChild( textarea1 );
card1.addEventListener('dragstart', dragstart1);
return card1;
}
function adjust_height1( textarea1 ) {
textarea1.style.height = 'auto';
textarea1.style.height = textarea1.scrollHeight + 'px';
}
function get_string1() {
let str1 = "";
str1 = str1 + '[BOARD_TITLE1]\n';
str1 = str1 + document.getElementById( "textbox0" ).value.trim() + '\n';
str1 = str1 + '[LIST_01]\n';
str1 = str1 + document.getElementById( "textbox1" ).value.trim() + '\n';
let children1 = document.getElementById( "list_1" ).children;
for (let i1 = 0; i1 < children1.length; i1++ ) {
let obj1 = children1[i1].querySelector('textarea');
if ( obj1 ) {
str1 = str1 + obj1.value.replace( "\n", "; " ).trim() + '\n';
}
}
str1 = str1 + '[LIST_02]\n';
str1 = str1 + document.getElementById( "textbox2" ).value.trim() + '\n';
children1 = document.getElementById( "list_2" ).children;
for (let i1 = 0; i1 < children1.length; i1++ ) {
obj1 = children1[i1].querySelector('textarea');
if ( obj1 ) {
str1 = str1 + obj1.value.replace( "\n", "; " ).trim() + '\n';
}
}
str1 = str1 + '[LIST_03]\n';
str1 = str1 + document.getElementById( "textbox3" ).value.trim() + '\n';
children1 = document.getElementById( "list_3" ).children;
for (let i1 = 0; i1 < children1.length; i1++ ) {
obj1 = children1[i1].querySelector('textarea');
if ( obj1 ) {
str1 = str1 + obj1.value.replace( "\n", "; " ).trim() + '\n';
}
}
str1 = "var str1_board = `\n" + str1.trim() + "\n`;";
return str1;
}
function set_textbox1(n1) {
if (n1 == 0) {
document.getElementById( "textbox0" ).value = ""
document.getElementById( "textbox1" ).value = ""
document.getElementById( "textbox2" ).value = ""
document.getElementById( "textbox3" ).value = ""
} else {
document.getElementById( "textbox0" ).value = "Kanban Board"
document.getElementById( "textbox1" ).value = "To Do"
document.getElementById( "textbox2" ).value = "In Progress"
document.getElementById( "textbox3" ).value = "Done"
}
}
function clear1(n1) {
set_textbox1(n1);
let obj1 = document.getElementById("trash1");
for (let i1 = 0; i1 < n1_card; i1++ ) {
let str1 = "card_id_" + String(i1+1);
let obj2 = document.getElementById(str1);
if (obj2) {
obj1.appendChild(obj2);
obj1.removeChild(obj2);
}
}
n1_card = 0;
}
function filename1() {
let obj1 = document.getElementById( "select1" );
let n1 = obj1.options[obj1.selectedIndex].value;
let file1 = "kanban_board" + String(n1) + ".js";
return file1
}
function load1() {
if (busy1 < 0) {
busy1 = 1;
load_js1();
setTimeout( set_board1, 50 );
}
}
function load_js1() {
str1_board = "";
let file1 = filename1();
try {
scr1 = document.createElement("script");
scr1.src = file1;
document.head.appendChild( scr1 );
scr1.onerror = function() {};
} catch (error) {}
}
function set_board1() {
if (str1_board == "") {
clear1(1);
} else {
clear1(0);
let str1 = str1_board;
let a1 = str1.trim().split( "\n" );
let id1 = "";
let str2 = "";
for (let i1 = 0; i1 < a1.length; i1++ ) {
str2 = String(a1[i1]).trim().replace( "; ", "\n" );
if (str2 == "[BOARD_TITLE1]") {
id1 = "textbox0";
str2 = "";
} else if ( str2 == "[LIST_01]") {
id1 = "textbox1";
str2 = "";
} else if ( str2 == "[LIST_02]") {
id1 = "textbox2";
str2 = "";
} else if ( str2 == "[LIST_03]") {
id1 = "textbox3";
str2 = "";
}
if (str2 == "") {
continue;
}
if ( id1 == "textbox0" ) {
document.getElementById( id1 ).value = str2;
id1 = "";
} else if ( id1 == "textbox1" ) {
document.getElementById( id1 ).value = str2;
id1 = "list_1";
} else if ( id1 == "textbox2" ) {
document.getElementById( id1 ).value = str2;
id1 = "list_2";
} else if ( id1 == "textbox3" ) {
document.getElementById( id1 ).value = str2;
id1 = "list_3";
} else if ( id1.includes("list_") ) {
addCard1( id1, str2 );
}
}
}
busy1 = -1;
document.getElementById('select1').focus();
}
function save1() {
let str1 = get_string1();
let file1 = filename1();
let blob1 = new Blob([str1], { type: "text/plain" });
let link1 = document.createElement("a");
link1.href = URL.createObjectURL( blob1 );
link1.download = file1;
link1.click();
}
</script>
</head>
<body>
<div class="header1">
<select class="select1" id="select1" >
<option value="01">01</option>
<option value="02">02</option>
<option value="03">03</option>
<option value="04">04</option>
<option value="05">05</option>
<option value="06">06</option>
<option value="07">07</option>
<option value="08">08</option>
<option value="09">09</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
<option value="13">13</option>
<option value="14">14</option>
<option value="15">15</option>
<option value="16">16</option>
<option value="17">17</option>
<option value="18">18</option>
<option value="19">19</option>
<option value="20">20</option>
</select>
<input type="text" class="textbox0" id="textbox0" name="textbox0" value="Kanban Board">
<div>
<div class="side-by-side1">
<div><button class="button0" onclick="load1()" >reload</button></div>
<div><button class="button0" onclick="save1()" >save</button></div>
<div><button class="button0" onclick="clear1(1)">clear</button></div>
<div class="trash1" id="trash1">Trash Bin</div>
</div>
</div>
</div>
<div class="body1">
<div class="list1" id="list_1">
<div class="side-by-side1">
<input type="text" class="textbox1" id="textbox1" name="textbox1" value="">
<button class="button1" onclick="addCard1('list_1', '')">+</button>
</div>
</div>
<div class="list1" id="list_2">
<div class="side-by-side1">
<input type="text" class="textbox1" id="textbox2" name="textbox2" value="">
<button class="button1" onclick="addCard1('list_2', '')">+</button>
</div>
</div>
<div class="list1" id="list_3">
<div class="side-by-side1">
<input type="text" class="textbox1" id="textbox3" name="textbox3" value="">
<button class="button1" onclick="addCard1('list_3', '')">+</button>
</div>
</div>
</div>
</body>
</html>