カンバンがタスク管理に良さそうだったから、やっつけで作ってみた。
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が重複する(後ほど更新)
- カンバン追加後テキストエリアがクリアされない
- カンバン追加テキストエリアを閉じた時、テキストエリアがクリアされない
