カンバンがタスク管理に良さそうだったから、やっつけで作ってみた。
jquery
Shadow DOM
FlexBox
D&D API
自作コンテキストメニュー
Save、Load、Del、カンバン追加用テキストエリア表示
バグは気分で修正する。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>パーソナルカンバン</title>
<link rel="stylesheet" href="./kanbanStyle.css">
<script src="./jquery.min.js"></script>
<script src="./kanban.js"></script>
</head>
<body>
<div id="flexWrap">
<div class="kanban" id="todoTask" dropzone="move">
<div class="kanban-title"><h2>TODO</h2></div>
</div>
<div class="kanban" id="doingTask" dropzone="move">
<div class="kanban-title"><h2>doing</h2></div>
</div>
<div class="kanban" id="backlogTask" dropzone="move">
<div class="kanban-title"><h2>backlog</h2></div>
</div>
</div>
<div id="kannbann-addItem" style="display:none">
<div id="kanban-add-wrap">
<button id="kanban-add-clz">x</button>
<button id="kanban-add-todo">TODO</button>
<button id="kanban-add-doing">doing</button>
<button id="kanban-add-backlog">backlog</button>
</div>
<textarea id="kannbann-addItem-inpuut" placeholder="1行目タイトル 2行目以降内容"></textarea>
</div>
<template id="tmpKanbanItem">
<div class="kanban-item" contextmenu="delKanban" draggable="true"><h3 class="kanban-item-title">title</h3><p class="kanban-text">text</p></div>
</template>
<template id="tmpContextMenu">
<li class="context-item" id="del" >Delete</li>
<li class="context-item desable">Move item<ul class="contextMenu-item">
<li class="context-item desable" id="moveup">moveup</li>
<li class="context-item desable" id="movedown">movedown</li>
</ul></li>
</template>
<template id="tmpContextMenuSaveLoad">
<li class="context-item" id="addItem">AddItem</li>
<li class="context-item" id="save">Save</li>
<li class="context-item" id="load">Load</li>
</template>
</body>
</html>
kanbanStyle.css
html,body{width:100%;height:100%;padding:0;margin:0;}
h1,h2,h3,h4,h5,h6{margin:0.5rem 0;padding:0;}
#flexWrap {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 100%;
height: 100%;
}
#todoTask{background: lightgray}
#doingTask{background: white}
#backlogTask
.kanban {
display: grid;
position:relative;
height: 100%;
grid-template-rows:2rem;
grid-auto-rows: 200px;
grid-gap: 1rem;
overflow-y:scroll;
}
.kanban-title {
display:flex;
height: 2rem;
}
.kanban-title > h2 {
margin:0;
padding:0;
}
.kanban-item {
margin: 0 1rem;
border: solid 2px #666;
border-radius: 5px;
}
.kanban-item-title {font-size: 1.2rem;}
.kanban-text {font-size: 0.8rem;}
#kannbann-addItem {
height: 200px;
position:fixed;
bottom:0;
width:calc(100% - 2rem);
margin: 0;
padding: 0 1rem 1rem;
background: gray;
}
#kanban-add-wrap{}
#kannbann-addItem-inpuut{
width: calc(100% - 4px - 2rem);
height: 170px;
resize:none;
outline: none;
margin:0;
padding: 0.5rem;
border: 2px solid #999;
}
.contextMenu {
position: absolute;
background: lightgray;
margin: 0;
padding: 5px;
}
#contextMenu {
position:absolute;
min-width: 120px;
margin:0;
}
.contextMenu-item {
list-style:none;
padding: 0;
border: 0.1px solid #ccc;
box-shadow:3px 3px 5px 0px rgba(0,0,0,0.5);
}
.context-item {
position: relative;
font-size: 0.8rem;
height: 1.0rem;
padding: 0.2rem;
background: #fff;
}
.context-item:hover {background: #c6d7ff;}
.contextMenu-item > hr {
border-top: 0.2px solid #ccc;
border-left:none;
border-right:none;
border-bottom:none;
margin:0;
padding: 1px;
background: #fff;
}
.context-item > .contextMenu-item {display: none;position: absolute;left: 100%;top: 0;}
.context-item:hover > .contextMenu-item {display: block;}
.desable{background: lightgray !important;}
kanban.js
$(function(){
loadKanban();
setEvents();
});
function setEvents(){
$('.kanban-item').each(function(idx, elem) {
elem.id = `kanban-item-${idx}`;
})
$('.kanban').on('dragover dragenter', function(e) {
e.preventDefault();
})
.off('contextmenu').on('contextmenu', kanbanContextMenuSaveLoad)
.on('drop', kanbanDropEv);
$('.kanban-title').on('click', showKanbanAdd);
$('#kanban-add-clz').on('click', hideKanbanAdd);
$('#kanban-add-todo').on('click', function(){addKanbanItem('#todoTask')});
$('#kanban-add-doing').on('click', function(){addKanbanItem('#doingTask')});
$('#kanban-add-backlog').on('click', function(){addKanbanItem('#backlogTask')});
addKanbanItemEvent();
}
function addKanbanItemEvent(){
$('.kanban-item')
.off('dragstart').on('dragstart', kanbanDragstartEv)
.off('contextmenu').on('contextmenu', kanbanContextMenu)
;
}
function kanbanDragstartEv(e){
var elem = $(this);
var kanbanID = elem.closest('.kanban').attr('id');
var itemID = `#${elem.attr('id')}`;
e.originalEvent.dataTransfer.setData('kanbanID', kanbanID);
e.originalEvent.dataTransfer.setData('itemID', itemID);
}
function evCanselAndBlockBubble(e){
e.preventDefault();
e.stopPropagation();
}
function kanbanContextMenuSaveLoad(e){
evCanselAndBlockBubble(e);
let elem = getContextMenuBase(e);
elem.append($(getKanbanContextMenuSaveLoad()));
$(this).closest('.kanban').append(elem);
elem.children().data('targetID', `#${e.currentTarget.id}`);
setContextMenuEvent(elem);
}
function kanbanContextMenu(e) {
evCanselAndBlockBubble(e);
let elem = getContextMenuBase(e);
let item = `${getKanbanContextMenuSaveLoad()}<hr>${getKanbanContextMenu()}`;
elem.append($(item));
$(this).closest('.kanban').append(elem);
elem.children().data('targetID', `#${e.currentTarget.id}`);
setContextMenuEvent(elem);
}
function getContextMenuBase(e){return $(`<ul id="contextMenu" class="contextMenu-item" style="top:${e.pageY - 5}px;left:${e.offsetX - 5}px;"></ul>`)}
function getKanbanContextMenuSaveLoad() {
let menuItem = $('#tmpContextMenuSaveLoad')[0].content.cloneNode(true);
return getKanbanContextMenuCreator(menuItem);
}
function getKanbanContextMenu(){
let menuItem = $('#tmpContextMenu')[0].content.cloneNode(true);
return getKanbanContextMenuCreator(menuItem);
}
function getKanbanContextMenuCreator(menuItem) {
let item = '';
for (let li of menuItem.children) item+= li.outerHTML;
return item;
}
function setContextMenuEvent(menuElem){
menuElem
.off('click')
.on('click', '#addItem', showKanbanAdd)
.on('click', '#save', saveKanban)
.on('click', '#load', loadKanban)
.on('click', '#del', function(e){kanbanDeltarget($(this).data('targetID'));})
.on('click', '#moveup', function(e){kanbanMoveUp($(this).data('targetID'));})
.on('click', '#movedown', function(e){kanbanMoveDown($(this).data('targetID'));})
.on('click', '.context-item', kanbanContextMenuRemove)
.off('mouseleave').on('mouseleave', kanbanContextMenuRemove)
;
}
function kanbanDeltarget(id){$(id).remove();}
function kanbanContextMenuRemove(){$('#contextMenu').remove();}
function kanbanMoveUp(id){
console.log(id);
}
function kanbanMoveDown(id){
console.log(id);
}
function kanbanDropEv(e) {
var kanbanID = e.originalEvent.dataTransfer.getData('kanbanID');
var itemID = e.originalEvent.dataTransfer.getData('itemID');
elem = $(itemID).clone();
$(itemID).remove();
$(this).append(elem);
addKanbanItemEvent();
}
function getNewItemID(){
return $('.kanban-item').length;
}
function saveKanban(){
var todoItem = [];
var doingItem = [];
var backlogItem = [];
$('#todoTask > .kanban-item').each(function(idx,elem){todoItem.push(retObj(idx, elem));})
$('#doingTask > .kanban-item').each(function(idx,elem){doingItem.push(retObj(idx, elem));})
$('#backlogTask > .kanban-item').each(function(idx,elem){backlogItem.push(retObj(idx, elem));})
let data = {
todoTask: todoItem,
doingTask: doingItem,
backlogTask: backlogItem,
};
localStorage.setItem('kanban', JSON.stringify(data));
}
function retObj(idx,elem) {
return {
id:elem.id,
title: elem.querySelector('.kanban-item-title').html(),
text: elem.querySelector('.kanban-text').html(),
}
}
function loadKanban() {
var data = JSON.parse(localStorage.getItem('kanban'));
for (var d in data) {
let tmp = '';
for (var f of data[d]) tmp += `<div class="kanban-item" contextmenu="delKanban" draggable="true" id="${f.id}"><h3 class="kanban-item-title">${f.title}</h3><p class="kanban-text">${f.text}</p></div>`;
$(`#${d} > .kanban-item`).remove();
$(`#${d}`).append(tmp);
}
addKanbanItemEvent();
}
function showKanbanAdd() {$('#kannbann-addItem').show();}
function hideKanbanAdd() {$('#kannbann-addItem').hide();$('#kannbann-addItem-inpuut').val('');}
function addKanbanItem(id) {
var data = $('#kannbann-addItem-inpuut').val().split(/\r|\n/);
var title = data.shift(), text = data.join('<br>');
let itemId = getNewItemID();
let elem = $(`<div class="kanban-item" contextmenu="delKanban" draggable="true" id="kanban-item-${itemId}"><h3 class="kanban-item-title">${title}</h3><p class="kanban-text">${text}</p></div>`);
$(id).append(elem);
addKanbanItemEvent();
}
function checkMe(me){
console.log(me);
}
実装予定
- D&Dの並び替え
- 登録済みカンバン編集機能
- 開始終了日付
- 任意のカンバン種類追加削除
- stylesheetのless化(less.js使用予定)
- ショートカットキーによる操作(内容未定)
- オートセーブ(ON/OFF切り替え付き)
既知のバグ
- カンバンの内容の長さに合わせてy軸方向に伸縮しない(後ほど更新)
- カンバンを削除→追加でカンバンに振っているIDが重複する(後ほど更新)
- カンバン追加後テキストエリアがクリアされない
- カンバン追加テキストエリアを閉じた時、テキストエリアがクリアされない